Bug 29755: Check each NoIssuesCharge separately
[koha.git] / C4 / SIP / ILS / Patron.pm
1 #
2 # ILS::Patron.pm
3
4 # A Class for hiding the ILS's concept of the patron from the OpenSIP
5 # system
6 #
7
8 package C4::SIP::ILS::Patron;
9
10 use strict;
11 use warnings;
12 use Exporter;
13 use Carp;
14
15 use C4::SIP::Sip qw(siplog);
16 use Data::Dumper;
17
18 use C4::SIP::Sip qw(add_field maybe_add);
19
20 use C4::Context;
21 use C4::Koha;
22 use C4::Members;
23 use C4::Reserves;
24 use C4::Auth qw(checkpw);
25
26 use Koha::Items;
27 use Koha::Libraries;
28 use Koha::Patrons;
29
30 our $kp;    # koha patron
31
32 =head1 Methods
33
34 =cut
35
36 sub new {
37     my ($class, $patron_id) = @_;
38     my $type = ref($class) || $class;
39     my $self;
40
41     my $patron;
42     if ( ref $patron_id eq "HASH" ) {
43         if ( $patron_id->{borrowernumber} ) {
44             $patron = Koha::Patrons->find( $patron_id->{borrowernumber} );
45         } elsif ( $patron_id->{cardnumber} ) {
46             $patron = Koha::Patrons->find( { cardnumber => $patron_id->{cardnumber} } );
47         } elsif ( $patron_id->{userid} ) {
48             $patron = Koha::Patrons->find( { userid => $patron_id->{userid} } );
49         }
50     } else {
51         $patron = Koha::Patrons->find( { cardnumber => $patron_id } )
52             || Koha::Patrons->find( { userid => $patron_id } );
53     }
54
55     unless ($patron) {
56         siplog("LOG_DEBUG", "new ILS::Patron(%s): no such patron", $patron_id);
57         return;
58     }
59     $kp = $patron->unblessed;
60     my $pw        = $kp->{password};
61     my $flags     = C4::Members::patronflags( $kp );
62     my $debarred  = $patron->is_debarred;
63     my ($day, $month, $year) = (localtime)[3,4,5];
64     my $today    = sprintf '%04d-%02d-%02d', $year+1900, $month+1, $day;
65     my $expired  = ($today gt $kp->{dateexpiry}) ? 1 : 0;
66     if ($expired) {
67         if ($kp->{opacnote} ) {
68             $kp->{opacnote} .= q{ };
69         }
70         $kp->{opacnote} .= 'PATRON EXPIRED';
71     }
72     my %ilspatron;
73     my $adr     = _get_address($kp);
74     my $dob     = $kp->{dateofbirth};
75     $dob and $dob =~ s/-//g;    # YYYYMMDD
76     my $dexpiry     = $kp->{dateexpiry};
77     $dexpiry and $dexpiry =~ s/-//g;    # YYYYMMDD
78
79     # Get fines and add fines for guarantees (depends on preference NoIssuesChargeGuarantees)
80     my $fines_amount = ($patron->account->balance > 0) ? $patron->account->non_issues_charges : 0;
81     my $fee_limit = _fee_limit();
82     my $fine_blocked = $fines_amount > $fee_limit;
83     my $noissueschargeguarantorswithguarantees = C4::Context->preference('NoIssuesChargeGuarantorsWithGuarantees');
84     my $noissueschargeguarantees = C4::Context->preference('NoIssuesChargeGuarantees');
85     if ( $noissueschargeguarantorswithguarantees ) {
86         $fines_amount += $patron->relationships_debt({ include_guarantors => 1, only_this_guarantor => 0, include_this_patron => 0 });
87         $fine_blocked ||= $fines_amount > $noissueschargeguarantorswithguarantees;
88     } elsif ( $noissueschargeguarantees ) {
89         $fines_amount += $patron->relationships_debt({ include_guarantors => 0, only_this_guarantor => 0, include_this_patron => 0 });
90         $fine_blocked ||= $fines_amount > $noissueschargeguarantees;
91     }
92
93     my $circ_blocked =( C4::Context->preference('OverduesBlockCirc') ne "noblock" &&  defined $flags->{ODUES}->{itemlist} ) ? 1 : 0;
94     {
95     no warnings;    # any of these $kp->{fields} being concat'd could be undef
96     %ilspatron = (
97         name => $kp->{firstname} . " " . $kp->{surname},
98         id   => $kp->{cardnumber},    # to SIP, the id is the BARCODE, not userid
99         password        => $pw,
100         ptype           => $kp->{categorycode},     # 'A'dult.  Whatever.
101         dateexpiry      => $dexpiry,
102         dateexpiry_iso  => $kp->{dateexpiry},
103         birthdate       => $dob,
104         birthdate_iso   => $kp->{dateofbirth},
105         branchcode      => $kp->{branchcode},
106         library_name    => "",                      # only populated if needed, cached here
107         borrowernumber  => $kp->{borrowernumber},
108         address         => $adr,
109         home_phone      => $kp->{phone},
110         email_addr      => $kp->{email},
111         charge_ok       => ( !$debarred && !$expired && !$fine_blocked && !$circ_blocked),
112         renew_ok        => ( !$debarred && !$expired && !$fine_blocked),
113         recall_ok       => ( !$debarred && !$expired && !$fine_blocked),
114         hold_ok         => ( !$debarred && !$expired && !$fine_blocked),
115         card_lost       => ( $kp->{lost} || $kp->{gonenoaddress} || $flags->{LOST} ),
116         claims_returned => 0,
117         fines           => $fines_amount,
118         fees            => 0,             # currently not distinct from fines
119         recall_overdue  => 0,
120         items_billed    => 0,
121         screen_msg      => 'Greetings from Koha. ' . $kp->{opacnote},
122         print_line      => '',
123         items           => [],
124         hold_items      => $flags->{WAITING}->{itemlist},
125         overdue_items   => $flags->{ODUES}->{itemlist},
126         too_many_overdue => $circ_blocked,
127         fine_items      => [],
128         recall_items    => [],
129         unavail_holds   => [],
130         inet            => ( !$debarred && !$expired ),
131         debarred        => $debarred,
132         expired         => $expired,
133         fine_blocked    => $fine_blocked,
134         fee_limit       => $fee_limit,
135         userid          => $kp->{userid},
136     );
137     }
138
139     if ( $patron->is_debarred and $patron->debarredcomment ) {
140         $ilspatron{screen_msg} .= " -- " . $patron->debarredcomment;
141     }
142     if ( $circ_blocked ) {
143         $ilspatron{screen_msg} .= " -- " . "Patron has overdues";
144     }
145     for (qw(EXPIRED CHARGES CREDITS GNA LOST NOTES)) {
146         ($flags->{$_}) or next;
147         if ($_ ne 'NOTES' and $flags->{$_}->{message}) {
148             $ilspatron{screen_msg} .= " -- " . $flags->{$_}->{message};  # show all but internal NOTES
149         }
150         if ($flags->{$_}->{noissues}) {
151             foreach my $toggle (qw(charge_ok renew_ok recall_ok hold_ok inet)) {
152                 $ilspatron{$toggle} = 0;    # if we get noissues, disable everything
153             }
154         }
155     }
156
157     # FIXME: populate fine_items recall_items
158     $ilspatron{unavail_holds} = _get_outstanding_holds($kp->{borrowernumber});
159
160     my $pending_checkouts = $patron->pending_checkouts;
161     my @barcodes;
162     while ( my $c = $pending_checkouts->next ) {
163         push @barcodes, { barcode => $c->item->barcode };
164     }
165     $ilspatron{items} = \@barcodes;
166
167     $self = \%ilspatron;
168     siplog("LOG_DEBUG", "new ILS::Patron(%s): found patron '%s'", $patron_id,$self->{id});
169     bless $self, $type;
170     return $self;
171 }
172
173
174 # 0 means read-only
175 # 1 means read/write
176
177 my %fields = (
178     id                      => 0,
179     borrowernumber          => 0,
180     name                    => 0,
181     address                 => 0,
182     email_addr              => 0,
183     home_phone              => 0,
184     birthdate               => 0,
185     birthdate_iso           => 0,
186     dateexpiry              => 0,
187     dateexpiry_iso          => 0,
188     debarred                => 0,
189     fine_blocked            => 0,
190     ptype                   => 0,
191     charge_ok               => 0,   # for patron_status[0] (inverted)
192     renew_ok                => 0,   # for patron_status[1] (inverted)
193     recall_ok               => 0,   # for patron_status[2] (inverted)
194     hold_ok                 => 0,   # for patron_status[3] (inverted)
195     card_lost               => 0,   # for patron_status[4]
196     recall_overdue          => 0,
197     currency                => 1,
198     fee_limit               => 0,
199     screen_msg              => 1,
200     print_line              => 1,
201     too_many_charged        => 0,   # for patron_status[5]
202     too_many_overdue        => 0,   # for patron_status[6]
203     too_many_renewal        => 0,   # for patron_status[7]
204     too_many_claim_return   => 0,   # for patron_status[8]
205     too_many_lost           => 0,   # for patron_status[9]
206 #   excessive_fines         => 0,   # for patron_status[10]
207 #   excessive_fees          => 0,   # for patron_status[11]
208     recall_overdue          => 0,   # for patron_status[12]
209     too_many_billed         => 0,   # for patron_status[13]
210     inet                    => 0,   # EnvisionWare extension
211 );
212
213 our $AUTOLOAD;
214
215 sub DESTROY {
216     # be cool.  needed for AUTOLOAD(?)
217 }
218
219 sub AUTOLOAD {
220     my $self = shift;
221     my $class = ref($self) or croak "$self is not an object";
222     my $name = $AUTOLOAD;
223
224     $name =~ s/.*://;
225
226     unless (exists $fields{$name}) {
227         croak "Cannot access '$name' field of class '$class'";
228     }
229
230     if (@_) {
231         $fields{$name} or croak "Field '$name' of class '$class' is READ ONLY.";
232         return $self->{$name} = shift;
233     } else {
234         return $self->{$name};
235     }
236 }
237
238 =head2 format
239
240 This method uses a template to build a string from a Koha::Patron object
241 If errors are encountered in processing template we log them and return nothing
242
243 =cut
244
245 sub format {
246     my ( $self, $template ) = @_;
247
248     if ($template) {
249         require Template;
250         require Koha::Patrons;
251
252         my $tt = Template->new();
253
254         my $patron = Koha::Patrons->find( $self->{borrowernumber} );
255
256         my $output;
257         eval {
258             $tt->process( \$template, { patron => $patron }, \$output );
259         };
260         if ( $@ ){
261             siplog("LOG_DEBUG", "Error processing template: $template");
262             return "";
263         }
264         return $output;
265     }
266 }
267
268 sub check_password {
269     my ( $self, $pwd ) = @_;
270
271     # you gotta give me something (at least ''), or no deal
272     return 0 unless defined $pwd;
273
274     # If the record has a NULL password, accept '' as match
275     return $pwd eq q{} unless $self->{password};
276
277     my $dbh = C4::Context->dbh;
278     my $ret = 0;
279     ($ret) = checkpw( $dbh, $self->{userid}, $pwd, undef, undef, 1 ); # dbh, userid, query, type, no_set_userenv
280     return $ret;
281 }
282
283 # A few special cases, not in AUTOLOADed %fields
284 sub fee_amount {
285     my $self = shift;
286     if ( $self->{fines} ) {
287         return $self->{fines};
288     }
289     return 0;
290 }
291
292 sub fines_amount {
293     my $self = shift;
294     return $self->fee_amount;
295 }
296
297 sub language {
298     my $self = shift;
299     return $self->{language} || '000'; # Unspecified
300 }
301
302 sub expired {
303     my $self = shift;
304     return $self->{expired};
305 }
306
307 #
308 # remove the hold on item item_id from my hold queue.
309 # return true if I was holding the item, false otherwise.
310
311 sub drop_hold {
312     my ($self, $item_id) = @_;
313     return if !$item_id;
314     my $result = 0;
315     foreach (qw(hold_items unavail_holds)) {
316         $self->{$_} or next;
317         for (my $i = 0; $i < scalar @{$self->{$_}}; $i++) {
318             my $held_item = $self->{$_}[$i]->{barcode} or next;
319             if ($held_item eq $item_id) {
320                 splice @{$self->{$_}}, $i, 1;
321                 $result++;
322             }
323         }
324     }
325     return $result;
326 }
327
328 # Accessor method for array_ref values, designed to get the "start" and "end" values
329 # from the SIP request.  Note those incoming values are 1-indexed, not 0-indexed.
330 #
331 sub x_items {
332     my $self      = shift;
333     my $array_var = shift or return;
334     my ($start, $end) = @_;
335
336     my $item_list = [];
337     if ($self->{$array_var}) {
338         if ($start && $start > 1) {
339             --$start;
340         }
341         else {
342             $start = 0;
343         }
344         if ( $end && $end < @{$self->{$array_var}} ) {
345         }
346         else {
347             $end = @{$self->{$array_var}};
348             --$end;
349         }
350         @{$item_list} = @{$self->{$array_var}}[ $start .. $end ];
351
352     }
353     return $item_list;
354 }
355
356 #
357 # List of outstanding holds placed
358 #
359 sub hold_items {
360     my $self = shift;
361     my $item_arr = $self->x_items('hold_items', @_);
362     foreach my $item (@{$item_arr}) {
363         my $item_obj = Koha::Items->find($item->{itemnumber});
364         $item->{barcode} = $item_obj ? $item_obj->barcode : undef;
365     }
366     return $item_arr;
367 }
368
369 sub overdue_items {
370     my $self = shift;
371     return $self->x_items('overdue_items', @_);
372 }
373 sub charged_items {
374     my $self = shift;
375     return $self->x_items('items', @_);
376 }
377 sub fine_items {
378     require Koha::Database;
379     require Template;
380
381     my $self = shift;
382     my $start = shift;
383     my $end = shift;
384     my $server = shift;
385
386     my @fees = Koha::Database->new()->schema()->resultset('Accountline')->search(
387         {
388             borrowernumber    => $self->{borrowernumber},
389             amountoutstanding => { '>' => '0' },
390         }
391     );
392
393     $start = $start ? $start - 1 : 0;
394     $end   = $end   ? $end - 1   : scalar @fees - 1;
395
396     my $av_field_template = $server ? $server->{account}->{av_field_template} : undef;
397     $av_field_template ||= "[% accountline.description %] [% accountline.amountoutstanding | format('%.2f') %]";
398
399     my $tt = Template->new();
400
401     my @return_values;
402     for ( my $i = $start; $i <= $end; $i++ ) {
403         my $fee = $fees[$i];
404
405         next unless $fee;
406
407         my $output;
408         $tt->process( \$av_field_template, { accountline => $fee }, \$output );
409         push( @return_values, { barcode => $output } );
410     }
411
412     return \@return_values;
413 }
414 sub recall_items {
415     my $self = shift;
416     return $self->x_items('recall_items', @_);
417 }
418 sub unavail_holds {
419     my $self = shift;
420     return $self->x_items('unavail_holds', @_);
421 }
422
423 sub block {
424     my ($self, $card_retained, $blocked_card_msg) = @_;
425     foreach my $field ('charge_ok', 'renew_ok', 'recall_ok', 'hold_ok', 'inet') {
426         $self->{$field} = 0;
427     }
428     $self->{screen_msg} = "Block feature not implemented";  # $blocked_card_msg || "Card Blocked.  Please contact library staff";
429     # TODO: not really affecting patron record
430     return $self;
431 }
432
433 sub enable {
434     my $self = shift;
435     foreach my $field ('charge_ok', 'renew_ok', 'recall_ok', 'hold_ok', 'inet') {
436         $self->{$field} = 1;
437     }
438     siplog("LOG_DEBUG", "Patron(%s)->enable: charge: %s, renew:%s, recall:%s, hold:%s",
439        $self->{id}, $self->{charge_ok}, $self->{renew_ok},
440        $self->{recall_ok}, $self->{hold_ok});
441     $self->{screen_msg} = "Enable feature not implemented."; # "All privileges restored.";   # TODO: not really affecting patron record
442     return $self;
443 }
444
445 sub inet_privileges {
446     my $self = shift;
447     return $self->{inet} ? 'Y' : 'N';
448 }
449
450 sub _fee_limit {
451     return C4::Context->preference('noissuescharge') || 5;
452 }
453
454 sub excessive_fees {
455     my $self = shift;
456     return ($self->fee_amount and $self->fee_amount > $self->fee_limit);
457 }
458
459 sub excessive_fines {
460     my $self = shift;
461     return $self->excessive_fees;   # excessive_fines is the same thing as excessive_fees for Koha
462 }
463
464 sub holds_blocked_by_excessive_fees {
465     my $self = shift;
466     return ( $self->fee_amount
467           && $self->fee_amount > C4::Context->preference("maxoutstanding") );
468 }
469     
470 sub library_name {
471     my $self = shift;
472     unless ($self->{library_name}) {
473         my $library = Koha::Libraries->find( $self->{branchcode} );
474         $self->{library_name} = $library ? $library->branchname : '';
475     }
476     return $self->{library_name};
477 }
478 #
479 # Messages
480 #
481
482 sub invalid_patron {
483     my $self = shift;
484     return "Please contact library staff";
485 }
486
487 sub charge_denied {
488     my $self = shift;
489     return "Please contact library staff";
490 }
491
492 =head2 update_lastseen
493
494     $patron->update_lastseen();
495
496     Patron method to update lastseen field in borrower
497     to record that patron has been seen via sip connection
498
499 =cut
500
501 sub update_lastseen {
502     my $self = shift;
503     my $kohaobj = Koha::Patrons->find( $self->{borrowernumber} );
504     $kohaobj->track_login if $kohaobj; # track_login checks the pref
505 }
506
507 sub _get_address {
508     my $patron = shift;
509
510     my $address = $patron->{streetnumber} || q{};
511     for my $field (qw( roaddetails address address2 city state zipcode country))
512     {
513         next unless $patron->{$field};
514         if ($address) {
515             $address .= q{ };
516             $address .= $patron->{$field};
517         }
518         else {
519             $address .= $patron->{$field};
520         }
521     }
522     return $address;
523 }
524
525 sub _get_outstanding_holds {
526     my $borrowernumber = shift;
527
528     my $patron = Koha::Patrons->find( $borrowernumber );
529     my $holds = $patron->holds->search( { -or => [ { found => undef }, { found => { '!=' => 'W' } } ] } );
530     my @holds;
531     while ( my $hold = $holds->next ) {
532         my $item;
533         if ($hold->itemnumber) {
534             $item = $hold->item;
535         }
536         else {
537             # We need to return a barcode for the biblio so the client
538             # can request the biblio info
539             my $items = $hold->biblio->items;
540             $item = $items->count ? $items->next : undef;
541         }
542         my $unblessed_hold = $hold->unblessed;
543
544         $unblessed_hold->{barcode} = $item ? $item->barcode : undef;
545
546         push @holds, $unblessed_hold;
547     }
548     return \@holds;
549 }
550
551 =head2 build_patron_attributes_string
552
553 This method builds the part of the sip message for extended patron
554 attributes as defined in the sip config
555
556 =cut
557
558 sub build_patron_attributes_string {
559     my ( $self, $server ) = @_;
560
561     my $string = q{};
562     if ( $server->{account}->{patron_attribute} ) {
563         my @attributes_to_send =
564           ref $server->{account}->{patron_attribute} eq "ARRAY"
565           ? @{ $server->{account}->{patron_attribute} }
566           : ( $server->{account}->{patron_attribute} );
567
568         foreach my $a ( @attributes_to_send ) {
569             my @attributes = Koha::Patron::Attributes->search(
570                 {
571                     borrowernumber => $self->{borrowernumber},
572                     code           => $a->{code}
573                 }
574             )->as_list;
575
576             foreach my $attribute ( @attributes ) {
577                 my $value = $attribute->attribute();
578                 $string .= add_field( $a->{field}, $value );
579             }
580         }
581     }
582
583     return $string;
584 }
585
586
587 =head2 build_custom_field_string
588
589 This method builds the part of the sip message for custom patron fields as defined in the sip config
590
591 =cut
592
593 sub build_custom_field_string {
594     my ( $self, $server ) = @_;
595
596     my $string = q{};
597
598     if ( $server->{account}->{custom_patron_field} ) {
599         my @custom_fields =
600             ref $server->{account}->{custom_patron_field} eq "ARRAY"
601             ? @{ $server->{account}->{custom_patron_field} }
602             : $server->{account}->{custom_patron_field};
603         foreach my $custom_field ( @custom_fields ) {
604             $string .= maybe_add( $custom_field->{field}, $self->format( $custom_field->{template} ) ) if defined $custom_field->{field};
605         }
606     }
607     return $string;
608 }
609
610 1;
611 __END__
612
613 =head1 EXAMPLES
614
615   our %patron_example = (
616           djfiander => {
617               name => "David J. Fiander",
618               id => 'djfiander',
619               password => '6789',
620               ptype => 'A', # 'A'dult.  Whatever.
621               birthdate => '19640925',
622               address => '2 Meadowvale Dr. St Thomas, ON',
623               home_phone => '(519) 555 1234',
624               email_addr => 'djfiander@hotmail.com',
625               charge_ok => 1,
626               renew_ok => 1,
627               recall_ok => 0,
628               hold_ok => 1,
629               card_lost => 0,
630               claims_returned => 0,
631               fines => 100,
632               fees => 0,
633               recall_overdue => 0,
634               items_billed => 0,
635               screen_msg => '',
636               print_line => '',
637               items => [],
638               hold_items => [],
639               overdue_items => [],
640               fine_items => ['Computer Time'],
641               recall_items => [],
642               unavail_holds => [],
643               inet => 1,
644           },
645   );
646
647  From borrowers table:
648 +---------------------+--------------+------+-----+---------+----------------+
649 | Field               | Type         | Null | Key | Default | Extra          |
650 +---------------------+--------------+------+-----+---------+----------------+
651 | borrowernumber      | int(11)      | NO   | PRI | NULL    | auto_increment |
652 | cardnumber          | varchar(16)  | YES  | UNI | NULL    |                |
653 | surname             | mediumtext   | NO   |     | NULL    |                |
654 | firstname           | text         | YES  |     | NULL    |                |
655 | title               | mediumtext   | YES  |     | NULL    |                |
656 | othernames          | mediumtext   | YES  |     | NULL    |                |
657 | initials            | text         | YES  |     | NULL    |                |
658 | streetnumber        | varchar(10)  | YES  |     | NULL    |                |
659 | streettype          | varchar(50)  | YES  |     | NULL    |                |
660 | address             | mediumtext   | NO   |     | NULL    |                |
661 | address2            | text         | YES  |     | NULL    |                |
662 | city                | mediumtext   | NO   |     | NULL    |                |
663 | state               | mediumtext   | YES  |     | NULL    |                |
664 | zipcode             | varchar(25)  | YES  |     | NULL    |                |
665 | country             | text         | YES  |     | NULL    |                |
666 | email               | mediumtext   | YES  |     | NULL    |                |
667 | phone               | text         | YES  |     | NULL    |                |
668 | mobile              | varchar(50)  | YES  |     | NULL    |                |
669 | fax                 | mediumtext   | YES  |     | NULL    |                |
670 | emailpro            | text         | YES  |     | NULL    |                |
671 | phonepro            | text         | YES  |     | NULL    |                |
672 | B_streetnumber      | varchar(10)  | YES  |     | NULL    |                |
673 | B_streettype        | varchar(50)  | YES  |     | NULL    |                |
674 | B_address           | varchar(100) | YES  |     | NULL    |                |
675 | B_address2          | text         | YES  |     | NULL    |                |
676 | B_city              | mediumtext   | YES  |     | NULL    |                |
677 | B_state             | mediumtext   | YES  |     | NULL    |                |
678 | B_zipcode           | varchar(25)  | YES  |     | NULL    |                |
679 | B_country           | text         | YES  |     | NULL    |                |
680 | B_email             | text         | YES  |     | NULL    |                |
681 | B_phone             | mediumtext   | YES  |     | NULL    |                |
682 | dateofbirth         | date         | YES  |     | NULL    |                |
683 | branchcode          | varchar(10)  | NO   | MUL |         |                |
684 | categorycode        | varchar(10)  | NO   | MUL |         |                |
685 | dateenrolled        | date         | YES  |     | NULL    |                |
686 | dateexpiry          | date         | YES  |     | NULL    |                |
687 | gonenoaddress       | tinyint(1)   | YES  |     | NULL    |                |
688 | lost                | tinyint(1)   | YES  |     | NULL    |                |
689 | debarred            | tinyint(1)   | YES  |     | NULL    |                |
690 | contactname         | mediumtext   | YES  |     | NULL    |                |
691 | contactfirstname    | text         | YES  |     | NULL    |                |
692 | contacttitle        | text         | YES  |     | NULL    |                |
693 | borrowernotes       | mediumtext   | YES  |     | NULL    |                |
694 | relationship        | varchar(100) | YES  |     | NULL    |                |
695 | ethnicity           | varchar(50)  | YES  |     | NULL    |                |
696 | ethnotes            | varchar(255) | YES  |     | NULL    |                |
697 | sex                 | varchar(1)   | YES  |     | NULL    |                |
698 | password            | varchar(30)  | YES  |     | NULL    |                |
699 | flags               | int(11)      | YES  |     | NULL    |                |
700 | userid              | varchar(30)  | YES  | MUL | NULL    |                |
701 | opacnote            | mediumtext   | YES  |     | NULL    |                |
702 | contactnote         | varchar(255) | YES  |     | NULL    |                |
703 | sort1               | varchar(80)  | YES  |     | NULL    |                |
704 | sort2               | varchar(80)  | YES  |     | NULL    |                |
705 | altcontactfirstname | varchar(255) | YES  |     | NULL    |                |
706 | altcontactsurname   | varchar(255) | YES  |     | NULL    |                |
707 | altcontactaddress1  | varchar(255) | YES  |     | NULL    |                |
708 | altcontactaddress2  | varchar(255) | YES  |     | NULL    |                |
709 | altcontactaddress3  | varchar(255) | YES  |     | NULL    |                |
710 | altcontactstate     | mediumtext   | YES  |     | NULL    |                |
711 | altcontactzipcode   | varchar(50)  | YES  |     | NULL    |                |
712 | altcontactcountry   | text         | YES  |     | NULL    |                |
713 | altcontactphone     | varchar(50)  | YES  |     | NULL    |                |
714 | smsalertnumber      | varchar(50)  | YES  |     | NULL    |                |
715 | privacy             | int(11)      | NO   |     | 1       |                |
716 +---------------------+--------------+------+-----+---------+----------------+
717
718
719  From C4::Members
720
721  $flags->{KEY}
722  {CHARGES}
723     {message}     Message showing patron's credit or debt
724     {noissues}    Set if patron owes >$5.00
725  {GNA}             Set if patron gone w/o address
726     {message}     "Borrower has no valid address"
727     {noissues}    Set.
728  {LOST}            Set if patron's card reported lost
729     {message}     Message to this effect
730     {noissues}    Set.
731  {DBARRED}         Set if patron is debarred
732     {message}     Message to this effect
733     {noissues}    Set.
734  {NOTES}           Set if patron has notes
735     {message}     Notes about patron
736  {ODUES}           Set if patron has overdue books
737     {message}     "Yes"
738     {itemlist}    ref-to-array: list of overdue books
739     {itemlisttext}    Text list of overdue items
740  {WAITING}         Set if there are items available that the patron reserved
741     {message}     Message to this effect
742     {itemlist}    ref-to-array: list of available items
743
744 =cut
745