4 use vars qw( @ISA $conf $Debug $import );
10 use Business::CreditCard;
11 use FS::UID qw( getotaker dbh );
12 use FS::Record qw( qsearchs qsearch dbdef );
15 use FS::cust_bill_pkg;
18 use FS::part_referral;
19 use FS::cust_main_county;
21 use FS::cust_main_invoice;
22 use FS::cust_credit_bill;
23 use FS::cust_bill_pay;
24 use FS::prepay_credit;
27 use FS::part_bill_event;
28 use FS::cust_bill_event;
29 use FS::msgcat qw(gettext);
31 @ISA = qw( FS::Record );
38 #ask FS::UID to run this stuff for us later
39 $FS::UID::callback{'FS::cust_main'} = sub {
41 #yes, need it for stuff below (prolly should be cached)
46 my ( $hashref, $cache ) = @_;
47 if ( exists $hashref->{'pkgnum'} ) {
48 # #@{ $self->{'_pkgnum'} } = ();
49 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
50 $self->{'_pkgnum'} = $subcache;
51 #push @{ $self->{'_pkgnum'} },
52 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
58 FS::cust_main - Object methods for cust_main records
64 $record = new FS::cust_main \%hash;
65 $record = new FS::cust_main { 'column' => 'value' };
67 $error = $record->insert;
69 $error = $new_record->replace($old_record);
71 $error = $record->delete;
73 $error = $record->check;
75 @cust_pkg = $record->all_pkgs;
77 @cust_pkg = $record->ncancelled_pkgs;
79 @cust_pkg = $record->suspended_pkgs;
81 $error = $record->bill;
82 $error = $record->bill %options;
83 $error = $record->bill 'time' => $time;
85 $error = $record->collect;
86 $error = $record->collect %options;
87 $error = $record->collect 'invoice_time' => $time,
88 'batch_card' => 'yes',
89 'report_badcard' => 'yes',
94 An FS::cust_main object represents a customer. FS::cust_main inherits from
95 FS::Record. The following fields are currently supported:
99 =item custnum - primary key (assigned automatically for new customers)
101 =item agentnum - agent (see L<FS::agent>)
103 =item refnum - Advertising source (see L<FS::part_referral>)
109 =item ss - social security number (optional)
111 =item company - (optional)
115 =item address2 - (optional)
119 =item county - (optional, see L<FS::cust_main_county>)
121 =item state - (see L<FS::cust_main_county>)
125 =item country - (see L<FS::cust_main_county>)
127 =item daytime - phone (optional)
129 =item night - phone (optional)
131 =item fax - phone (optional)
133 =item ship_first - name
135 =item ship_last - name
137 =item ship_company - (optional)
141 =item ship_address2 - (optional)
145 =item ship_county - (optional, see L<FS::cust_main_county>)
147 =item ship_state - (see L<FS::cust_main_county>)
151 =item ship_country - (see L<FS::cust_main_county>)
153 =item ship_daytime - phone (optional)
155 =item ship_night - phone (optional)
157 =item ship_fax - phone (optional)
159 =item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
161 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
163 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
165 =item payname - name on card or billing name
167 =item tax - tax exempt, empty or `Y'
169 =item otaker - order taker (assigned automatically, see L<FS::UID>)
171 =item comments - comments (optional)
181 Creates a new customer. To add the customer to the database, see L<"insert">.
183 Note that this stores the hash reference, not a distinct copy of the hash it
184 points to. You can ask the object for a copy with the I<hash> method.
188 sub table { 'cust_main'; }
190 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
192 Adds this customer to the database. If there is an error, returns the error,
193 otherwise returns false.
195 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
196 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
197 are inserted atomicly, or the transaction is rolled back. Passing an empty
198 hash reference is equivalent to not supplying this parameter. There should be
199 a better explanation of this, but until then, here's an example:
202 tie %hash, 'Tie::RefHash'; #this part is important
204 $cust_pkg => [ $svc_acct ],
207 $cust_main->insert( \%hash );
209 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
210 be set as the invoicing list (see L<"invoicing_list">). Errors return as
211 expected and rollback the entire transaction; it is not necessary to call
212 check_invoicing_list first. The invoicing_list is set after the records in the
213 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
214 invoicing_list destination to the newly-created svc_acct. Here's an example:
216 $cust_main->insert( {}, [ $email, 'POST' ] );
224 local $SIG{HUP} = 'IGNORE';
225 local $SIG{INT} = 'IGNORE';
226 local $SIG{QUIT} = 'IGNORE';
227 local $SIG{TERM} = 'IGNORE';
228 local $SIG{TSTP} = 'IGNORE';
229 local $SIG{PIPE} = 'IGNORE';
231 my $oldAutoCommit = $FS::UID::AutoCommit;
232 local $FS::UID::AutoCommit = 0;
237 if ( $self->payby eq 'PREPAY' ) {
238 $self->payby('BILL');
239 my $prepay_credit = qsearchs(
241 { 'identifier' => $self->payinfo },
245 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
246 unless $prepay_credit;
247 $amount = $prepay_credit->amount;
248 $seconds = $prepay_credit->seconds;
249 my $error = $prepay_credit->delete;
251 $dbh->rollback if $oldAutoCommit;
252 return "removing prepay_credit (transaction rolled back): $error";
256 my $error = $self->SUPER::insert;
258 $dbh->rollback if $oldAutoCommit;
259 return "inserting cust_main record (transaction rolled back): $error";
262 if ( @param ) { # CUST_PKG_HASHREF
263 my $cust_pkgs = shift @param;
264 foreach my $cust_pkg ( keys %$cust_pkgs ) {
265 $cust_pkg->custnum( $self->custnum );
266 $error = $cust_pkg->insert;
268 $dbh->rollback if $oldAutoCommit;
269 return "inserting cust_pkg (transaction rolled back): $error";
271 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
272 $svc_something->pkgnum( $cust_pkg->pkgnum );
273 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
274 $svc_something->seconds( $svc_something->seconds + $seconds );
277 $error = $svc_something->insert;
279 $dbh->rollback if $oldAutoCommit;
280 return "inserting svc_ (transaction rolled back): $error";
287 $dbh->rollback if $oldAutoCommit;
288 return "No svc_acct record to apply pre-paid time";
291 if ( @param ) { # INVOICING_LIST_ARYREF
292 my $invoicing_list = shift @param;
293 $error = $self->check_invoicing_list( $invoicing_list );
295 $dbh->rollback if $oldAutoCommit;
296 return "checking invoicing_list (transaction rolled back): $error";
298 $self->invoicing_list( $invoicing_list );
302 my $cust_credit = new FS::cust_credit {
303 'custnum' => $self->custnum,
306 $error = $cust_credit->insert;
308 $dbh->rollback if $oldAutoCommit;
309 return "inserting credit (transaction rolled back): $error";
313 #false laziness with sub replace
314 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
315 $error = $queue->insert($self->getfield('last'), $self->company);
317 $dbh->rollback if $oldAutoCommit;
318 return "queueing job (transaction rolled back): $error";
321 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
322 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
323 $error = $queue->insert($self->getfield('last'), $self->company);
325 $dbh->rollback if $oldAutoCommit;
326 return "queueing job (transaction rolled back): $error";
331 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
336 =item delete NEW_CUSTNUM
338 This deletes the customer. If there is an error, returns the error, otherwise
341 This will completely remove all traces of the customer record. This is not
342 what you want when a customer cancels service; for that, cancel all of the
343 customer's packages (see L<FS::cust_pkg/cancel>).
345 If the customer has any uncancelled packages, you need to pass a new (valid)
346 customer number for those packages to be transferred to. Cancelled packages
347 will be deleted. Did I mention that this is NOT what you want when a customer
348 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
350 You can't delete a customer with invoices (see L<FS::cust_bill>),
351 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
352 refunds (see L<FS::cust_refund>).
359 local $SIG{HUP} = 'IGNORE';
360 local $SIG{INT} = 'IGNORE';
361 local $SIG{QUIT} = 'IGNORE';
362 local $SIG{TERM} = 'IGNORE';
363 local $SIG{TSTP} = 'IGNORE';
364 local $SIG{PIPE} = 'IGNORE';
366 my $oldAutoCommit = $FS::UID::AutoCommit;
367 local $FS::UID::AutoCommit = 0;
370 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
371 $dbh->rollback if $oldAutoCommit;
372 return "Can't delete a customer with invoices";
374 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
375 $dbh->rollback if $oldAutoCommit;
376 return "Can't delete a customer with credits";
378 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
379 $dbh->rollback if $oldAutoCommit;
380 return "Can't delete a customer with payments";
382 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
383 $dbh->rollback if $oldAutoCommit;
384 return "Can't delete a customer with refunds";
387 my @cust_pkg = $self->ncancelled_pkgs;
389 my $new_custnum = shift;
390 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
391 $dbh->rollback if $oldAutoCommit;
392 return "Invalid new customer number: $new_custnum";
394 foreach my $cust_pkg ( @cust_pkg ) {
395 my %hash = $cust_pkg->hash;
396 $hash{'custnum'} = $new_custnum;
397 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
398 my $error = $new_cust_pkg->replace($cust_pkg);
400 $dbh->rollback if $oldAutoCommit;
405 my @cancelled_cust_pkg = $self->all_pkgs;
406 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
407 my $error = $cust_pkg->delete;
409 $dbh->rollback if $oldAutoCommit;
414 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
415 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
417 my $error = $cust_main_invoice->delete;
419 $dbh->rollback if $oldAutoCommit;
424 my $error = $self->SUPER::delete;
426 $dbh->rollback if $oldAutoCommit;
430 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
435 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
437 Replaces the OLD_RECORD with this one in the database. If there is an error,
438 returns the error, otherwise returns false.
440 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
441 be set as the invoicing list (see L<"invoicing_list">). Errors return as
442 expected and rollback the entire transaction; it is not necessary to call
443 check_invoicing_list first. Here's an example:
445 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
454 local $SIG{HUP} = 'IGNORE';
455 local $SIG{INT} = 'IGNORE';
456 local $SIG{QUIT} = 'IGNORE';
457 local $SIG{TERM} = 'IGNORE';
458 local $SIG{TSTP} = 'IGNORE';
459 local $SIG{PIPE} = 'IGNORE';
461 my $oldAutoCommit = $FS::UID::AutoCommit;
462 local $FS::UID::AutoCommit = 0;
465 my $error = $self->SUPER::replace($old);
468 $dbh->rollback if $oldAutoCommit;
472 if ( @param ) { # INVOICING_LIST_ARYREF
473 my $invoicing_list = shift @param;
474 $error = $self->check_invoicing_list( $invoicing_list );
476 $dbh->rollback if $oldAutoCommit;
479 $self->invoicing_list( $invoicing_list );
482 #false laziness with sub insert
483 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
484 $error = $queue->insert($self->getfield('last'), $self->company);
486 $dbh->rollback if $oldAutoCommit;
487 return "queueing job (transaction rolled back): $error";
490 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
491 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
492 $error = $queue->insert($self->getfield('last'), $self->company);
494 $dbh->rollback if $oldAutoCommit;
495 return "queueing job (transaction rolled back): $error";
500 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
507 Checks all fields to make sure this is a valid customer record. If there is
508 an error, returns the error, otherwise returns false. Called by the insert
516 #warn "BEFORE: \n". $self->_dump;
519 $self->ut_numbern('custnum')
520 || $self->ut_number('agentnum')
521 || $self->ut_number('refnum')
522 || $self->ut_name('last')
523 || $self->ut_name('first')
524 || $self->ut_textn('company')
525 || $self->ut_text('address1')
526 || $self->ut_textn('address2')
527 || $self->ut_text('city')
528 || $self->ut_textn('county')
529 || $self->ut_textn('state')
530 || $self->ut_country('country')
531 || $self->ut_anything('comments')
532 || $self->ut_numbern('referral_custnum')
534 #barf. need message catalogs. i18n. etc.
535 $error .= "Please select a advertising source."
536 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
537 return $error if $error;
539 return "Unknown agent"
540 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
542 return "Unknown refnum"
543 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
545 return "Unknown referring custnum ". $self->referral_custnum
546 unless ! $self->referral_custnum
547 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
549 if ( $self->ss eq '' ) {
554 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
555 or return "Illegal social security number: ". $self->ss;
556 $self->ss("$1-$2-$3");
560 # bad idea to disable, causes billing to fail because of no tax rates later
561 # unless ( $import ) {
562 unless ( qsearchs('cust_main_county', {
563 'country' => $self->country,
566 return "Unknown state/county/country: ".
567 $self->state. "/". $self->county. "/". $self->country
568 unless qsearchs('cust_main_county',{
569 'state' => $self->state,
570 'county' => $self->county,
571 'country' => $self->country,
577 $self->ut_phonen('daytime', $self->country)
578 || $self->ut_phonen('night', $self->country)
579 || $self->ut_phonen('fax', $self->country)
580 || $self->ut_zip('zip', $self->country)
582 return $error if $error;
585 last first company address1 address2 city county state zip
586 country daytime night fax
589 if ( defined $self->dbdef_table->column('ship_last') ) {
590 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
592 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
596 $self->ut_name('ship_last')
597 || $self->ut_name('ship_first')
598 || $self->ut_textn('ship_company')
599 || $self->ut_text('ship_address1')
600 || $self->ut_textn('ship_address2')
601 || $self->ut_text('ship_city')
602 || $self->ut_textn('ship_county')
603 || $self->ut_textn('ship_state')
604 || $self->ut_country('ship_country')
606 return $error if $error;
608 #false laziness with above
609 unless ( qsearchs('cust_main_county', {
610 'country' => $self->ship_country,
613 return "Unknown ship_state/ship_county/ship_country: ".
614 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
615 unless qsearchs('cust_main_county',{
616 'state' => $self->ship_state,
617 'county' => $self->ship_county,
618 'country' => $self->ship_country,
624 $self->ut_phonen('ship_daytime', $self->ship_country)
625 || $self->ut_phonen('ship_night', $self->ship_country)
626 || $self->ut_phonen('ship_fax', $self->ship_country)
627 || $self->ut_zip('ship_zip', $self->ship_country)
629 return $error if $error;
631 } else { # ship_ info eq billing info, so don't store dup info in database
632 $self->setfield("ship_$_", '')
633 foreach qw( last first company address1 address2 city county state zip
634 country daytime night fax );
638 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
639 or return "Illegal payby: ". $self->payby;
642 if ( $self->payby eq 'CARD' ) {
644 my $payinfo = $self->payinfo;
646 $payinfo =~ /^(\d{13,16})$/
647 or return gettext('invalid_catd'); # . ": ". $self->payinfo;
649 $self->payinfo($payinfo);
651 or return gettext('invalid_catd'); # . ": ". $self->payinfo;
652 return gettext('unknown_card_type')
653 if cardtype($self->payinfo) eq "Unknown";
655 } elsif ( $self->payby eq 'BILL' ) {
657 $error = $self->ut_textn('payinfo');
658 return "Illegal P.O. number: ". $self->payinfo if $error;
660 } elsif ( $self->payby eq 'COMP' ) {
662 $error = $self->ut_textn('payinfo');
663 return "Illegal comp account issuer: ". $self->payinfo if $error;
665 } elsif ( $self->payby eq 'PREPAY' ) {
667 my $payinfo = $self->payinfo;
668 $payinfo =~ s/\W//g; #anything else would just confuse things
669 $self->payinfo($payinfo);
670 $error = $self->ut_alpha('payinfo');
671 return "Illegal prepayment identifier: ". $self->payinfo if $error;
672 return "Unknown prepayment identifier"
673 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
677 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
678 return "Expriation date required"
679 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
682 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
683 or return "Illegal expiration date: ". $self->paydate;
684 if ( length($2) == 4 ) {
685 $self->paydate("$2-$1-01");
687 $self->paydate("20$2-$1-01");
691 if ( $self->payname eq '' ) {
692 $self->payname( $self->first. " ". $self->getfield('last') );
694 $self->payname =~ /^([\w \,\.\-\']+)$/
695 or return gettext('illegal_name'). " payname: ". $self->payname;
699 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
702 $self->otaker(getotaker);
704 #warn "AFTER: \n". $self->_dump;
711 Returns all packages (see L<FS::cust_pkg>) for this customer.
717 if ( $self->{'_pkgnum'} ) {
718 values %{ $self->{'_pkgnum'}->cache };
720 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
724 =item ncancelled_pkgs
726 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
730 sub ncancelled_pkgs {
732 if ( $self->{'_pkgnum'} ) {
733 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
735 @{ [ # force list context
736 qsearch( 'cust_pkg', {
737 'custnum' => $self->custnum,
740 qsearch( 'cust_pkg', {
741 'custnum' => $self->custnum,
750 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
756 grep { $_->susp } $self->ncancelled_pkgs;
759 =item unflagged_suspended_pkgs
761 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
762 customer (thouse packages without the `manual_flag' set).
766 sub unflagged_suspended_pkgs {
768 return $self->suspended_pkgs
769 unless dbdef->table('cust_pkg')->column('manual_flag');
770 grep { ! $_->manual_flag } $self->suspended_pkgs;
773 =item unsuspended_pkgs
775 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
780 sub unsuspended_pkgs {
782 grep { ! $_->susp } $self->ncancelled_pkgs;
787 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
788 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
789 on success or a list of errors.
795 grep { $_->unsuspend } $self->suspended_pkgs;
800 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
801 Always returns a list: an empty list on success or a list of errors.
807 grep { $_->suspend } $self->unsuspended_pkgs;
812 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
813 Always returns a list: an empty list on success or a list of errors.
819 grep { $_->cancel } $self->ncancelled_pkgs;
824 Returns the agent (see L<FS::agent>) for this customer.
830 qsearchs( 'agent', { 'agent' => $self->agentnum } );
835 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
836 conjunction with the collect method.
838 Options are passed as name-value pairs.
840 The only currently available option is `time', which bills the customer as if
841 it were that time. It is specified as a UNIX timestamp; see
842 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
843 functions. For example:
847 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
849 If there is an error, returns the error, otherwise returns false.
854 my( $self, %options ) = @_;
855 my $time = $options{'time'} || time;
860 local $SIG{HUP} = 'IGNORE';
861 local $SIG{INT} = 'IGNORE';
862 local $SIG{QUIT} = 'IGNORE';
863 local $SIG{TERM} = 'IGNORE';
864 local $SIG{TSTP} = 'IGNORE';
865 local $SIG{PIPE} = 'IGNORE';
867 my $oldAutoCommit = $FS::UID::AutoCommit;
868 local $FS::UID::AutoCommit = 0;
871 # find the packages which are due for billing, find out how much they are
872 # & generate invoice database.
874 my( $total_setup, $total_recur ) = ( 0, 0 );
875 my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
876 my @cust_bill_pkg = ();
878 foreach my $cust_pkg (
879 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
882 #NO!! next if $cust_pkg->cancel;
883 next if $cust_pkg->getfield('cancel');
885 #? to avoid use of uninitialized value errors... ?
886 $cust_pkg->setfield('bill', '')
887 unless defined($cust_pkg->bill);
889 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
891 #so we don't modify cust_pkg record unnecessarily
892 my $cust_pkg_mod_flag = 0;
893 my %hash = $cust_pkg->hash;
894 my $old_cust_pkg = new FS::cust_pkg \%hash;
898 unless ( $cust_pkg->setup ) {
899 my $setup_prog = $part_pkg->getfield('setup');
900 $setup_prog =~ /^(.*)$/ or do {
901 $dbh->rollback if $oldAutoCommit;
902 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
908 ##$cpt->permit(); #what is necessary?
909 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
910 #$setup = $cpt->reval($setup_prog);
911 $setup = eval $setup_prog;
912 unless ( defined($setup) ) {
913 $dbh->rollback if $oldAutoCommit;
914 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
915 "(expression $setup_prog): $@";
917 $cust_pkg->setfield('setup',$time);
918 $cust_pkg_mod_flag=1;
924 if ( $part_pkg->getfield('freq') > 0 &&
925 ! $cust_pkg->getfield('susp') &&
926 ( $cust_pkg->getfield('bill') || 0 ) < $time
928 my $recur_prog = $part_pkg->getfield('recur');
929 $recur_prog =~ /^(.*)$/ or do {
930 $dbh->rollback if $oldAutoCommit;
931 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
936 # shared with $recur_prog
937 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
940 ##$cpt->permit(); #what is necessary?
941 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
942 #$recur = $cpt->reval($recur_prog);
943 $recur = eval $recur_prog;
944 unless ( defined($recur) ) {
945 $dbh->rollback if $oldAutoCommit;
946 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
947 "(expression $recur_prog): $@";
949 #change this bit to use Date::Manip? CAREFUL with timezones (see
950 # mailing list archive)
951 my ($sec,$min,$hour,$mday,$mon,$year) =
952 (localtime($sdate) )[0,1,2,3,4,5];
954 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
955 # only for figuring next bill date, nothing else, so, reset $sdate again
957 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
959 $mon += $part_pkg->getfield('freq');
960 until ( $mon < 12 ) { $mon -= 12; $year++; }
961 $cust_pkg->setfield('bill',
962 timelocal($sec,$min,$hour,$mday,$mon,$year));
963 $cust_pkg_mod_flag = 1;
966 warn "\$setup is undefined" unless defined($setup);
967 warn "\$recur is undefined" unless defined($recur);
968 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
970 if ( $cust_pkg_mod_flag ) {
971 $error=$cust_pkg->replace($old_cust_pkg);
972 if ( $error ) { #just in case
973 $dbh->rollback if $oldAutoCommit;
974 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
976 $setup = sprintf( "%.2f", $setup );
977 $recur = sprintf( "%.2f", $recur );
979 $dbh->rollback if $oldAutoCommit;
980 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
983 $dbh->rollback if $oldAutoCommit;
984 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
986 if ( $setup > 0 || $recur > 0 ) {
987 my $cust_bill_pkg = new FS::cust_bill_pkg ({
988 'pkgnum' => $cust_pkg->pkgnum,
992 'edate' => $cust_pkg->bill,
994 push @cust_bill_pkg, $cust_bill_pkg;
995 $total_setup += $setup;
996 $total_recur += $recur;
997 $taxable_setup += $setup
998 unless $part_pkg->dbdef_table->column('setuptax')
999 && $part_pkg->setuptax =~ /^Y$/i;
1000 $taxable_recur += $recur
1001 unless $part_pkg->dbdef_table->column('recurtax')
1002 && $part_pkg->recurtax =~ /^Y$/i;
1008 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1009 my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1011 unless ( @cust_bill_pkg ) {
1012 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1016 unless ( $self->tax =~ /Y/i
1017 || $self->payby eq 'COMP'
1018 || $taxable_charged == 0 ) {
1019 my $cust_main_county = qsearchs('cust_main_county',{
1020 'state' => $self->state,
1021 'county' => $self->county,
1022 'country' => $self->country,
1023 } ) or die "fatal: can't find tax rate for state/county/country ".
1024 $self->state. "/". $self->county. "/". $self->country. "\n";
1025 my $tax = sprintf( "%.2f",
1026 $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1030 $charged = sprintf( "%.2f", $charged+$tax );
1032 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1039 push @cust_bill_pkg, $cust_bill_pkg;
1043 my $cust_bill = new FS::cust_bill ( {
1044 'custnum' => $self->custnum,
1046 'charged' => $charged,
1048 $error = $cust_bill->insert;
1050 $dbh->rollback if $oldAutoCommit;
1051 return "can't create invoice for customer #". $self->custnum. ": $error";
1054 my $invnum = $cust_bill->invnum;
1056 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1058 $cust_bill_pkg->invnum($invnum);
1059 $error = $cust_bill_pkg->insert;
1061 $dbh->rollback if $oldAutoCommit;
1062 return "can't create invoice line item for customer #". $self->custnum.
1067 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1071 =item collect OPTIONS
1073 (Attempt to) collect money for this customer's outstanding invoices (see
1074 L<FS::cust_bill>). Usually used after the bill method.
1076 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1077 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1079 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1080 and the invoice events web interface.
1082 If there is an error, returns the error, otherwise returns false.
1084 Options are passed as name-value pairs.
1086 Currently available options are:
1088 invoice_time - Use this time when deciding when to print invoices and
1089 late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse>
1090 for conversion functions.
1092 batch_card - This option is deprecated. See the invoice events web interface
1093 to control whether cards are batched or run against a realtime gateway.
1095 report_badcard - This option is deprecated.
1097 force_print - This option is deprecated; see the invoice events web interface.
1102 my( $self, %options ) = @_;
1103 my $invoice_time = $options{'invoice_time'} || time;
1106 local $SIG{HUP} = 'IGNORE';
1107 local $SIG{INT} = 'IGNORE';
1108 local $SIG{QUIT} = 'IGNORE';
1109 local $SIG{TERM} = 'IGNORE';
1110 local $SIG{TSTP} = 'IGNORE';
1111 local $SIG{PIPE} = 'IGNORE';
1113 my $oldAutoCommit = $FS::UID::AutoCommit;
1114 local $FS::UID::AutoCommit = 0;
1117 my $balance = $self->balance;
1118 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1119 unless ( $balance > 0 ) { #redundant?????
1120 $dbh->rollback if $oldAutoCommit; #hmm
1124 foreach my $cust_bill (
1125 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1128 #this has to be before next's
1129 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1133 $balance = sprintf( "%.2f", $balance - $amount );
1135 next unless $cust_bill->owed > 0;
1137 # don't try to charge for the same invoice if it's already in a batch
1138 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1140 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1142 next unless $amount > 0;
1144 foreach my $part_bill_event (
1145 sort { $a->seconds <=> $b->seconds
1146 || $a->weight <=> $b->weight
1147 || $a->eventpart <=> $b->eventpart }
1148 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1149 && ! qsearchs( 'cust_bill_event', {
1150 'invnum' => $cust_bill->invnum,
1151 'eventpart' => $_->eventpart,
1155 qsearch('part_bill_event', { 'payby' => $self->payby,
1156 'disabled' => '', } )
1159 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1161 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1163 my $cust_main = $self; #for callback
1164 my $error = eval $part_bill_event->eventcode;
1167 my $statustext = '';
1171 } elsif ( $error ) {
1173 $statustext = $error;
1178 #add cust_bill_event
1179 my $cust_bill_event = new FS::cust_bill_event {
1180 'invnum' => $cust_bill->invnum,
1181 'eventpart' => $part_bill_event->eventpart,
1182 '_date' => $invoice_time,
1183 'status' => $status,
1184 'statustext' => $statustext,
1186 $error = $cust_bill_event->insert;
1188 #$dbh->rollback if $oldAutoCommit;
1189 #return "error: $error";
1191 # gah, even with transactions.
1192 $dbh->commit if $oldAutoCommit; #well.
1193 my $e = 'WARNING: Event run but database not updated - '.
1194 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1195 ', eventpart '. $part_bill_event->eventpart.
1206 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1213 Returns the total owed for this customer on all invoices
1214 (see L<FS::cust_bill/owed>).
1220 $self->total_owed_date(2145859200); #12/31/2037
1223 =item total_owed_date TIME
1225 Returns the total owed for this customer on all invoices with date earlier than
1226 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1227 see L<Time::Local> and L<Date::Parse> for conversion functions.
1231 sub total_owed_date {
1235 foreach my $cust_bill (
1236 grep { $_->_date <= $time }
1237 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1239 $total_bill += $cust_bill->owed;
1241 sprintf( "%.2f", $total_bill );
1246 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1247 to outstanding invoice balances in chronological order and returns the value
1248 of any remaining unapplied credits available for refund
1249 (see L<FS::cust_refund>).
1256 return 0 unless $self->total_credited;
1258 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1259 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1261 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1262 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1266 foreach my $cust_bill ( @invoices ) {
1269 if ( !defined($credit) || $credit->credited == 0) {
1270 $credit = pop @credits or last;
1273 if ($cust_bill->owed >= $credit->credited) {
1274 $amount=$credit->credited;
1276 $amount=$cust_bill->owed;
1279 my $cust_credit_bill = new FS::cust_credit_bill ( {
1280 'crednum' => $credit->crednum,
1281 'invnum' => $cust_bill->invnum,
1282 'amount' => $amount,
1284 my $error = $cust_credit_bill->insert;
1285 die $error if $error;
1287 redo if ($cust_bill->owed > 0);
1291 return $self->total_credited;
1294 =item apply_payments
1296 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1297 to outstanding invoice balances in chronological order.
1299 #and returns the value of any remaining unapplied payments.
1303 sub apply_payments {
1308 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1309 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1311 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1312 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1316 foreach my $cust_bill ( @invoices ) {
1319 if ( !defined($payment) || $payment->unapplied == 0 ) {
1320 $payment = pop @payments or last;
1323 if ( $cust_bill->owed >= $payment->unapplied ) {
1324 $amount = $payment->unapplied;
1326 $amount = $cust_bill->owed;
1329 my $cust_bill_pay = new FS::cust_bill_pay ( {
1330 'paynum' => $payment->paynum,
1331 'invnum' => $cust_bill->invnum,
1332 'amount' => $amount,
1334 my $error = $cust_bill_pay->insert;
1335 die $error if $error;
1337 redo if ( $cust_bill->owed > 0);
1341 return $self->total_unapplied_payments;
1344 =item total_credited
1346 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1347 customer. See L<FS::cust_credit/credited>.
1351 sub total_credited {
1353 my $total_credit = 0;
1354 foreach my $cust_credit ( qsearch('cust_credit', {
1355 'custnum' => $self->custnum,
1357 $total_credit += $cust_credit->credited;
1359 sprintf( "%.2f", $total_credit );
1362 =item total_unapplied_payments
1364 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1365 See L<FS::cust_pay/unapplied>.
1369 sub total_unapplied_payments {
1371 my $total_unapplied = 0;
1372 foreach my $cust_pay ( qsearch('cust_pay', {
1373 'custnum' => $self->custnum,
1375 $total_unapplied += $cust_pay->unapplied;
1377 sprintf( "%.2f", $total_unapplied );
1382 Returns the balance for this customer (total_owed minus total_credited
1383 minus total_unapplied_payments).
1390 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1394 =item balance_date TIME
1396 Returns the balance for this customer, only considering invoices with date
1397 earlier than TIME (total_owed_date minus total_credited minus
1398 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1399 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1408 $self->total_owed_date($time)
1409 - $self->total_credited
1410 - $self->total_unapplied_payments
1414 =item invoicing_list [ ARRAYREF ]
1416 If an arguement is given, sets these email addresses as invoice recipients
1417 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1418 (except as warnings), so use check_invoicing_list first.
1420 Returns a list of email addresses (with svcnum entries expanded).
1422 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1423 check it without disturbing anything by passing nothing.
1425 This interface may change in the future.
1429 sub invoicing_list {
1430 my( $self, $arrayref ) = @_;
1432 my @cust_main_invoice;
1433 if ( $self->custnum ) {
1434 @cust_main_invoice =
1435 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1437 @cust_main_invoice = ();
1439 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1440 #warn $cust_main_invoice->destnum;
1441 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1442 #warn $cust_main_invoice->destnum;
1443 my $error = $cust_main_invoice->delete;
1444 warn $error if $error;
1447 if ( $self->custnum ) {
1448 @cust_main_invoice =
1449 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1451 @cust_main_invoice = ();
1453 my %seen = map { $_->address => 1 } @cust_main_invoice;
1454 foreach my $address ( @{$arrayref} ) {
1455 #unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1456 next if exists $seen{$address} && $seen{$address};
1457 $seen{$address} = 1;
1458 my $cust_main_invoice = new FS::cust_main_invoice ( {
1459 'custnum' => $self->custnum,
1462 my $error = $cust_main_invoice->insert;
1463 warn $error if $error;
1466 if ( $self->custnum ) {
1468 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1474 =item check_invoicing_list ARRAYREF
1476 Checks these arguements as valid input for the invoicing_list method. If there
1477 is an error, returns the error, otherwise returns false.
1481 sub check_invoicing_list {
1482 my( $self, $arrayref ) = @_;
1483 foreach my $address ( @{$arrayref} ) {
1484 my $cust_main_invoice = new FS::cust_main_invoice ( {
1485 'custnum' => $self->custnum,
1488 my $error = $self->custnum
1489 ? $cust_main_invoice->check
1490 : $cust_main_invoice->checkdest
1492 return $error if $error;
1497 =item default_invoicing_list
1499 Sets the invoicing list to all accounts associated with this customer.
1503 sub default_invoicing_list {
1506 foreach my $cust_pkg ( $self->all_pkgs ) {
1507 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1509 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1510 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1512 push @list, map { $_->email } @svc_acct;
1514 $self->invoicing_list(\@list);
1517 =item invoicing_list_addpost
1519 Adds postal invoicing to this customer. If this customer is already configured
1520 to receive postal invoices, does nothing.
1524 sub invoicing_list_addpost {
1526 return if grep { $_ eq 'POST' } $self->invoicing_list;
1527 my @invoicing_list = $self->invoicing_list;
1528 push @invoicing_list, 'POST';
1529 $self->invoicing_list(\@invoicing_list);
1532 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1534 Returns an array of customers referred by this customer (referral_custnum set
1535 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1536 customers referred by customers referred by this customer and so on, inclusive.
1537 The default behavior is DEPTH 1 (no recursion).
1541 sub referral_cust_main {
1543 my $depth = @_ ? shift : 1;
1544 my $exclude = @_ ? shift : {};
1547 map { $exclude->{$_->custnum}++; $_; }
1548 grep { ! $exclude->{ $_->custnum } }
1549 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1553 map { $_->referral_cust_main($depth-1, $exclude) }
1560 =item referral_cust_main_ncancelled
1562 Same as referral_cust_main, except only returns customers with uncancelled
1567 sub referral_cust_main_ncancelled {
1569 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
1572 =item referral_cust_pkg [ DEPTH ]
1574 Like referral_cust_main, except returns a flat list of all unsuspended (and
1575 uncancelled) packages for each customer. The number of items in this list may
1576 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
1580 sub referral_cust_pkg {
1582 my $depth = @_ ? shift : 1;
1584 map { $_->unsuspended_pkgs }
1585 grep { $_->unsuspended_pkgs }
1586 $self->referral_cust_main($depth);
1589 =item credit AMOUNT, REASON
1591 Applies a credit to this customer. If there is an error, returns the error,
1592 otherwise returns false.
1597 my( $self, $amount, $reason ) = @_;
1598 my $cust_credit = new FS::cust_credit {
1599 'custnum' => $self->custnum,
1600 'amount' => $amount,
1601 'reason' => $reason,
1603 $cust_credit->insert;
1606 =item charge AMOUNT PKG COMMENT
1608 Creates a one-time charge for this customer. If there is an error, returns
1609 the error, otherwise returns false.
1614 my ( $self, $amount, $pkg, $comment ) = @_;
1616 my $part_pkg = new FS::part_pkg ( {
1617 'pkg' => $pkg || 'One-time charge',
1618 'comment' => $comment || '$'. sprintf("%.2f".$amount),
1635 =item check_and_rebuild_fuzzyfiles
1639 sub check_and_rebuild_fuzzyfiles {
1640 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1641 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1642 or &rebuild_fuzzyfiles;
1645 =item rebuild_fuzzyfiles
1649 sub rebuild_fuzzyfiles {
1651 use Fcntl qw(:flock);
1653 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1657 open(LASTLOCK,">>$dir/cust_main.last")
1658 or die "can't open $dir/cust_main.last: $!";
1659 flock(LASTLOCK,LOCK_EX)
1660 or die "can't lock $dir/cust_main.last: $!";
1662 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1664 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1665 if defined dbdef->table('cust_main')->column('ship_last');
1667 open (LASTCACHE,">$dir/cust_main.last.tmp")
1668 or die "can't open $dir/cust_main.last.tmp: $!";
1669 print LASTCACHE join("\n", @all_last), "\n";
1670 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1672 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1677 open(COMPANYLOCK,">>$dir/cust_main.company")
1678 or die "can't open $dir/cust_main.company: $!";
1679 flock(COMPANYLOCK,LOCK_EX)
1680 or die "can't lock $dir/cust_main.company: $!";
1682 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1684 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1685 if defined dbdef->table('cust_main')->column('ship_last');
1687 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1688 or die "can't open $dir/cust_main.company.tmp: $!";
1689 print COMPANYCACHE join("\n", @all_company), "\n";
1690 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1692 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1702 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1703 open(LASTCACHE,"<$dir/cust_main.last")
1704 or die "can't open $dir/cust_main.last: $!";
1705 my @array = map { chomp; $_; } <LASTCACHE>;
1715 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1716 open(COMPANYCACHE,"<$dir/cust_main.company")
1717 or die "can't open $dir/cust_main.last: $!";
1718 my @array = map { chomp; $_; } <COMPANYCACHE>;
1723 =item append_fuzzyfiles LASTNAME COMPANY
1727 sub append_fuzzyfiles {
1728 my( $last, $company ) = @_;
1730 &check_and_rebuild_fuzzyfiles;
1732 use Fcntl qw(:flock);
1734 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1738 open(LAST,">>$dir/cust_main.last")
1739 or die "can't open $dir/cust_main.last: $!";
1741 or die "can't lock $dir/cust_main.last: $!";
1743 print LAST "$last\n";
1746 or die "can't unlock $dir/cust_main.last: $!";
1752 open(COMPANY,">>$dir/cust_main.company")
1753 or die "can't open $dir/cust_main.company: $!";
1754 flock(COMPANY,LOCK_EX)
1755 or die "can't lock $dir/cust_main.company: $!";
1757 print COMPANY "$company\n";
1759 flock(COMPANY,LOCK_UN)
1760 or die "can't unlock $dir/cust_main.company: $!";
1774 The delete method should possibly take an FS::cust_main object reference
1775 instead of a scalar customer number.
1777 Bill and collect options should probably be passed as references instead of a
1780 There should probably be a configuration file with a list of allowed credit
1783 No multiple currency support (probably a larger project than just this module).
1787 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1788 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
1789 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.