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";
263 if ( @param ) { # CUST_PKG_HASHREF
264 my $cust_pkgs = shift @param;
265 foreach my $cust_pkg ( keys %$cust_pkgs ) {
266 $cust_pkg->custnum( $self->custnum );
267 $error = $cust_pkg->insert;
269 $dbh->rollback if $oldAutoCommit;
270 return "inserting cust_pkg (transaction rolled back): $error";
272 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
273 $svc_something->pkgnum( $cust_pkg->pkgnum );
274 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
275 $svc_something->seconds( $svc_something->seconds + $seconds );
278 $error = $svc_something->insert;
280 $dbh->rollback if $oldAutoCommit;
281 #return "inserting svc_ (transaction rolled back): $error";
289 $dbh->rollback if $oldAutoCommit;
290 return "No svc_acct record to apply pre-paid time";
293 if ( @param ) { # INVOICING_LIST_ARYREF
294 my $invoicing_list = shift @param;
295 $error = $self->check_invoicing_list( $invoicing_list );
297 $dbh->rollback if $oldAutoCommit;
298 return "checking invoicing_list (transaction rolled back): $error";
300 $self->invoicing_list( $invoicing_list );
304 my $cust_credit = new FS::cust_credit {
305 'custnum' => $self->custnum,
308 $error = $cust_credit->insert;
310 $dbh->rollback if $oldAutoCommit;
311 return "inserting credit (transaction rolled back): $error";
315 #false laziness with sub replace
316 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
317 $error = $queue->insert($self->getfield('last'), $self->company);
319 $dbh->rollback if $oldAutoCommit;
320 return "queueing job (transaction rolled back): $error";
323 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
324 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
325 $error = $queue->insert($self->getfield('last'), $self->company);
327 $dbh->rollback if $oldAutoCommit;
328 return "queueing job (transaction rolled back): $error";
333 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
338 =item delete NEW_CUSTNUM
340 This deletes the customer. If there is an error, returns the error, otherwise
343 This will completely remove all traces of the customer record. This is not
344 what you want when a customer cancels service; for that, cancel all of the
345 customer's packages (see L<FS::cust_pkg/cancel>).
347 If the customer has any uncancelled packages, you need to pass a new (valid)
348 customer number for those packages to be transferred to. Cancelled packages
349 will be deleted. Did I mention that this is NOT what you want when a customer
350 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
352 You can't delete a customer with invoices (see L<FS::cust_bill>),
353 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
354 refunds (see L<FS::cust_refund>).
361 local $SIG{HUP} = 'IGNORE';
362 local $SIG{INT} = 'IGNORE';
363 local $SIG{QUIT} = 'IGNORE';
364 local $SIG{TERM} = 'IGNORE';
365 local $SIG{TSTP} = 'IGNORE';
366 local $SIG{PIPE} = 'IGNORE';
368 my $oldAutoCommit = $FS::UID::AutoCommit;
369 local $FS::UID::AutoCommit = 0;
372 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
373 $dbh->rollback if $oldAutoCommit;
374 return "Can't delete a customer with invoices";
376 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
377 $dbh->rollback if $oldAutoCommit;
378 return "Can't delete a customer with credits";
380 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
381 $dbh->rollback if $oldAutoCommit;
382 return "Can't delete a customer with payments";
384 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
385 $dbh->rollback if $oldAutoCommit;
386 return "Can't delete a customer with refunds";
389 my @cust_pkg = $self->ncancelled_pkgs;
391 my $new_custnum = shift;
392 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
393 $dbh->rollback if $oldAutoCommit;
394 return "Invalid new customer number: $new_custnum";
396 foreach my $cust_pkg ( @cust_pkg ) {
397 my %hash = $cust_pkg->hash;
398 $hash{'custnum'} = $new_custnum;
399 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
400 my $error = $new_cust_pkg->replace($cust_pkg);
402 $dbh->rollback if $oldAutoCommit;
407 my @cancelled_cust_pkg = $self->all_pkgs;
408 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
409 my $error = $cust_pkg->delete;
411 $dbh->rollback if $oldAutoCommit;
416 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
417 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
419 my $error = $cust_main_invoice->delete;
421 $dbh->rollback if $oldAutoCommit;
426 my $error = $self->SUPER::delete;
428 $dbh->rollback if $oldAutoCommit;
432 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
437 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
439 Replaces the OLD_RECORD with this one in the database. If there is an error,
440 returns the error, otherwise returns false.
442 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
443 be set as the invoicing list (see L<"invoicing_list">). Errors return as
444 expected and rollback the entire transaction; it is not necessary to call
445 check_invoicing_list first. Here's an example:
447 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
456 local $SIG{HUP} = 'IGNORE';
457 local $SIG{INT} = 'IGNORE';
458 local $SIG{QUIT} = 'IGNORE';
459 local $SIG{TERM} = 'IGNORE';
460 local $SIG{TSTP} = 'IGNORE';
461 local $SIG{PIPE} = 'IGNORE';
463 my $oldAutoCommit = $FS::UID::AutoCommit;
464 local $FS::UID::AutoCommit = 0;
467 my $error = $self->SUPER::replace($old);
470 $dbh->rollback if $oldAutoCommit;
474 if ( @param ) { # INVOICING_LIST_ARYREF
475 my $invoicing_list = shift @param;
476 $error = $self->check_invoicing_list( $invoicing_list );
478 $dbh->rollback if $oldAutoCommit;
481 $self->invoicing_list( $invoicing_list );
484 #false laziness with sub insert
485 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
486 $error = $queue->insert($self->getfield('last'), $self->company);
488 $dbh->rollback if $oldAutoCommit;
489 return "queueing job (transaction rolled back): $error";
492 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
493 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
494 $error = $queue->insert($self->getfield('last'), $self->company);
496 $dbh->rollback if $oldAutoCommit;
497 return "queueing job (transaction rolled back): $error";
502 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
509 Checks all fields to make sure this is a valid customer record. If there is
510 an error, returns the error, otherwise returns false. Called by the insert
518 #warn "BEFORE: \n". $self->_dump;
521 $self->ut_numbern('custnum')
522 || $self->ut_number('agentnum')
523 || $self->ut_number('refnum')
524 || $self->ut_name('last')
525 || $self->ut_name('first')
526 || $self->ut_textn('company')
527 || $self->ut_text('address1')
528 || $self->ut_textn('address2')
529 || $self->ut_text('city')
530 || $self->ut_textn('county')
531 || $self->ut_textn('state')
532 || $self->ut_country('country')
533 || $self->ut_anything('comments')
534 || $self->ut_numbern('referral_custnum')
536 #barf. need message catalogs. i18n. etc.
537 $error .= "Please select a advertising source."
538 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
539 return $error if $error;
541 return "Unknown agent"
542 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
544 return "Unknown refnum"
545 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
547 return "Unknown referring custnum ". $self->referral_custnum
548 unless ! $self->referral_custnum
549 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
551 if ( $self->ss eq '' ) {
556 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
557 or return "Illegal social security number: ". $self->ss;
558 $self->ss("$1-$2-$3");
562 # bad idea to disable, causes billing to fail because of no tax rates later
563 # unless ( $import ) {
564 unless ( qsearchs('cust_main_county', {
565 'country' => $self->country,
568 return "Unknown state/county/country: ".
569 $self->state. "/". $self->county. "/". $self->country
570 unless qsearchs('cust_main_county',{
571 'state' => $self->state,
572 'county' => $self->county,
573 'country' => $self->country,
579 $self->ut_phonen('daytime', $self->country)
580 || $self->ut_phonen('night', $self->country)
581 || $self->ut_phonen('fax', $self->country)
582 || $self->ut_zip('zip', $self->country)
584 return $error if $error;
587 last first company address1 address2 city county state zip
588 country daytime night fax
591 if ( defined $self->dbdef_table->column('ship_last') ) {
592 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
594 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
598 $self->ut_name('ship_last')
599 || $self->ut_name('ship_first')
600 || $self->ut_textn('ship_company')
601 || $self->ut_text('ship_address1')
602 || $self->ut_textn('ship_address2')
603 || $self->ut_text('ship_city')
604 || $self->ut_textn('ship_county')
605 || $self->ut_textn('ship_state')
606 || $self->ut_country('ship_country')
608 return $error if $error;
610 #false laziness with above
611 unless ( qsearchs('cust_main_county', {
612 'country' => $self->ship_country,
615 return "Unknown ship_state/ship_county/ship_country: ".
616 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
617 unless qsearchs('cust_main_county',{
618 'state' => $self->ship_state,
619 'county' => $self->ship_county,
620 'country' => $self->ship_country,
626 $self->ut_phonen('ship_daytime', $self->ship_country)
627 || $self->ut_phonen('ship_night', $self->ship_country)
628 || $self->ut_phonen('ship_fax', $self->ship_country)
629 || $self->ut_zip('ship_zip', $self->ship_country)
631 return $error if $error;
633 } else { # ship_ info eq billing info, so don't store dup info in database
634 $self->setfield("ship_$_", '')
635 foreach qw( last first company address1 address2 city county state zip
636 country daytime night fax );
640 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
641 or return "Illegal payby: ". $self->payby;
644 if ( $self->payby eq 'CARD' ) {
646 my $payinfo = $self->payinfo;
648 $payinfo =~ /^(\d{13,16})$/
649 or return gettext('invalid_card'); # . ": ". $self->payinfo;
651 $self->payinfo($payinfo);
653 or return gettext('invalid_card'); # . ": ". $self->payinfo;
654 return gettext('unknown_card_type')
655 if cardtype($self->payinfo) eq "Unknown";
657 } elsif ( $self->payby eq 'BILL' ) {
659 $error = $self->ut_textn('payinfo');
660 return "Illegal P.O. number: ". $self->payinfo if $error;
662 } elsif ( $self->payby eq 'COMP' ) {
664 $error = $self->ut_textn('payinfo');
665 return "Illegal comp account issuer: ". $self->payinfo if $error;
667 } elsif ( $self->payby eq 'PREPAY' ) {
669 my $payinfo = $self->payinfo;
670 $payinfo =~ s/\W//g; #anything else would just confuse things
671 $self->payinfo($payinfo);
672 $error = $self->ut_alpha('payinfo');
673 return "Illegal prepayment identifier: ". $self->payinfo if $error;
674 return "Unknown prepayment identifier"
675 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
679 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
680 return "Expriation date required"
681 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
684 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
685 or return "Illegal expiration date: ". $self->paydate;
686 if ( length($2) == 4 ) {
687 $self->paydate("$2-$1-01");
689 $self->paydate("20$2-$1-01");
693 if ( $self->payname eq '' ) {
694 $self->payname( $self->first. " ". $self->getfield('last') );
696 $self->payname =~ /^([\w \,\.\-\']+)$/
697 or return gettext('illegal_name'). " payname: ". $self->payname;
701 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
704 $self->otaker(getotaker);
706 #warn "AFTER: \n". $self->_dump;
713 Returns all packages (see L<FS::cust_pkg>) for this customer.
719 if ( $self->{'_pkgnum'} ) {
720 values %{ $self->{'_pkgnum'}->cache };
722 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
726 =item ncancelled_pkgs
728 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
732 sub ncancelled_pkgs {
734 if ( $self->{'_pkgnum'} ) {
735 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
737 @{ [ # force list context
738 qsearch( 'cust_pkg', {
739 'custnum' => $self->custnum,
742 qsearch( 'cust_pkg', {
743 'custnum' => $self->custnum,
752 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
758 grep { $_->susp } $self->ncancelled_pkgs;
761 =item unflagged_suspended_pkgs
763 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
764 customer (thouse packages without the `manual_flag' set).
768 sub unflagged_suspended_pkgs {
770 return $self->suspended_pkgs
771 unless dbdef->table('cust_pkg')->column('manual_flag');
772 grep { ! $_->manual_flag } $self->suspended_pkgs;
775 =item unsuspended_pkgs
777 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
782 sub unsuspended_pkgs {
784 grep { ! $_->susp } $self->ncancelled_pkgs;
789 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
790 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
791 on success or a list of errors.
797 grep { $_->unsuspend } $self->suspended_pkgs;
802 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
803 Always returns a list: an empty list on success or a list of errors.
809 grep { $_->suspend } $self->unsuspended_pkgs;
814 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
815 Always returns a list: an empty list on success or a list of errors.
821 grep { $_->cancel } $self->ncancelled_pkgs;
826 Returns the agent (see L<FS::agent>) for this customer.
832 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
837 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
838 conjunction with the collect method.
840 Options are passed as name-value pairs.
842 The only currently available option is `time', which bills the customer as if
843 it were that time. It is specified as a UNIX timestamp; see
844 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
845 functions. For example:
849 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
851 If there is an error, returns the error, otherwise returns false.
856 my( $self, %options ) = @_;
857 my $time = $options{'time'} || time;
862 local $SIG{HUP} = 'IGNORE';
863 local $SIG{INT} = 'IGNORE';
864 local $SIG{QUIT} = 'IGNORE';
865 local $SIG{TERM} = 'IGNORE';
866 local $SIG{TSTP} = 'IGNORE';
867 local $SIG{PIPE} = 'IGNORE';
869 my $oldAutoCommit = $FS::UID::AutoCommit;
870 local $FS::UID::AutoCommit = 0;
873 # find the packages which are due for billing, find out how much they are
874 # & generate invoice database.
876 my( $total_setup, $total_recur ) = ( 0, 0 );
877 my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
878 my @cust_bill_pkg = ();
880 foreach my $cust_pkg (
881 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
884 #NO!! next if $cust_pkg->cancel;
885 next if $cust_pkg->getfield('cancel');
887 #? to avoid use of uninitialized value errors... ?
888 $cust_pkg->setfield('bill', '')
889 unless defined($cust_pkg->bill);
891 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
893 #so we don't modify cust_pkg record unnecessarily
894 my $cust_pkg_mod_flag = 0;
895 my %hash = $cust_pkg->hash;
896 my $old_cust_pkg = new FS::cust_pkg \%hash;
900 unless ( $cust_pkg->setup ) {
901 my $setup_prog = $part_pkg->getfield('setup');
902 $setup_prog =~ /^(.*)$/ or do {
903 $dbh->rollback if $oldAutoCommit;
904 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
910 ##$cpt->permit(); #what is necessary?
911 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
912 #$setup = $cpt->reval($setup_prog);
913 $setup = eval $setup_prog;
914 unless ( defined($setup) ) {
915 $dbh->rollback if $oldAutoCommit;
916 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
917 "(expression $setup_prog): $@";
919 $cust_pkg->setfield('setup',$time);
920 $cust_pkg_mod_flag=1;
926 if ( $part_pkg->getfield('freq') > 0 &&
927 ! $cust_pkg->getfield('susp') &&
928 ( $cust_pkg->getfield('bill') || 0 ) < $time
930 my $recur_prog = $part_pkg->getfield('recur');
931 $recur_prog =~ /^(.*)$/ or do {
932 $dbh->rollback if $oldAutoCommit;
933 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
938 # shared with $recur_prog
939 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
942 ##$cpt->permit(); #what is necessary?
943 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
944 #$recur = $cpt->reval($recur_prog);
945 $recur = eval $recur_prog;
946 unless ( defined($recur) ) {
947 $dbh->rollback if $oldAutoCommit;
948 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
949 "(expression $recur_prog): $@";
951 #change this bit to use Date::Manip? CAREFUL with timezones (see
952 # mailing list archive)
953 my ($sec,$min,$hour,$mday,$mon,$year) =
954 (localtime($sdate) )[0,1,2,3,4,5];
956 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
957 # only for figuring next bill date, nothing else, so, reset $sdate again
959 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
961 $mon += $part_pkg->getfield('freq');
962 until ( $mon < 12 ) { $mon -= 12; $year++; }
963 $cust_pkg->setfield('bill',
964 timelocal($sec,$min,$hour,$mday,$mon,$year));
965 $cust_pkg_mod_flag = 1;
968 warn "\$setup is undefined" unless defined($setup);
969 warn "\$recur is undefined" unless defined($recur);
970 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
972 if ( $cust_pkg_mod_flag ) {
973 $error=$cust_pkg->replace($old_cust_pkg);
974 if ( $error ) { #just in case
975 $dbh->rollback if $oldAutoCommit;
976 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
978 $setup = sprintf( "%.2f", $setup );
979 $recur = sprintf( "%.2f", $recur );
981 $dbh->rollback if $oldAutoCommit;
982 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
985 $dbh->rollback if $oldAutoCommit;
986 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
988 if ( $setup > 0 || $recur > 0 ) {
989 my $cust_bill_pkg = new FS::cust_bill_pkg ({
990 'pkgnum' => $cust_pkg->pkgnum,
994 'edate' => $cust_pkg->bill,
996 push @cust_bill_pkg, $cust_bill_pkg;
997 $total_setup += $setup;
998 $total_recur += $recur;
999 $taxable_setup += $setup
1000 unless $part_pkg->dbdef_table->column('setuptax')
1001 && $part_pkg->setuptax =~ /^Y$/i;
1002 $taxable_recur += $recur
1003 unless $part_pkg->dbdef_table->column('recurtax')
1004 && $part_pkg->recurtax =~ /^Y$/i;
1010 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1011 my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1013 unless ( @cust_bill_pkg ) {
1014 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1018 unless ( $self->tax =~ /Y/i
1019 || $self->payby eq 'COMP'
1020 || $taxable_charged == 0 ) {
1021 my $cust_main_county = qsearchs('cust_main_county',{
1022 'state' => $self->state,
1023 'county' => $self->county,
1024 'country' => $self->country,
1025 } ) or die "fatal: can't find tax rate for state/county/country ".
1026 $self->state. "/". $self->county. "/". $self->country. "\n";
1027 my $tax = sprintf( "%.2f",
1028 $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1032 $charged = sprintf( "%.2f", $charged+$tax );
1034 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1041 push @cust_bill_pkg, $cust_bill_pkg;
1045 my $cust_bill = new FS::cust_bill ( {
1046 'custnum' => $self->custnum,
1048 'charged' => $charged,
1050 $error = $cust_bill->insert;
1052 $dbh->rollback if $oldAutoCommit;
1053 return "can't create invoice for customer #". $self->custnum. ": $error";
1056 my $invnum = $cust_bill->invnum;
1058 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1060 $cust_bill_pkg->invnum($invnum);
1061 $error = $cust_bill_pkg->insert;
1063 $dbh->rollback if $oldAutoCommit;
1064 return "can't create invoice line item for customer #". $self->custnum.
1069 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1073 =item collect OPTIONS
1075 (Attempt to) collect money for this customer's outstanding invoices (see
1076 L<FS::cust_bill>). Usually used after the bill method.
1078 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1079 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1081 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1082 and the invoice events web interface.
1084 If there is an error, returns the error, otherwise returns false.
1086 Options are passed as name-value pairs.
1088 Currently available options are:
1090 invoice_time - Use this time when deciding when to print invoices and
1091 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>
1092 for conversion functions.
1094 batch_card - This option is deprecated. See the invoice events web interface
1095 to control whether cards are batched or run against a realtime gateway.
1097 report_badcard - This option is deprecated.
1099 force_print - This option is deprecated; see the invoice events web interface.
1104 my( $self, %options ) = @_;
1105 my $invoice_time = $options{'invoice_time'} || time;
1108 local $SIG{HUP} = 'IGNORE';
1109 local $SIG{INT} = 'IGNORE';
1110 local $SIG{QUIT} = 'IGNORE';
1111 local $SIG{TERM} = 'IGNORE';
1112 local $SIG{TSTP} = 'IGNORE';
1113 local $SIG{PIPE} = 'IGNORE';
1115 my $oldAutoCommit = $FS::UID::AutoCommit;
1116 local $FS::UID::AutoCommit = 0;
1119 my $balance = $self->balance;
1120 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1121 unless ( $balance > 0 ) { #redundant?????
1122 $dbh->rollback if $oldAutoCommit; #hmm
1126 foreach my $cust_bill (
1127 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1130 #this has to be before next's
1131 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1135 $balance = sprintf( "%.2f", $balance - $amount );
1137 next unless $cust_bill->owed > 0;
1139 # don't try to charge for the same invoice if it's already in a batch
1140 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1142 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1144 next unless $amount > 0;
1146 foreach my $part_bill_event (
1147 sort { $a->seconds <=> $b->seconds
1148 || $a->weight <=> $b->weight
1149 || $a->eventpart <=> $b->eventpart }
1150 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1151 && ! qsearchs( 'cust_bill_event', {
1152 'invnum' => $cust_bill->invnum,
1153 'eventpart' => $_->eventpart,
1157 qsearch('part_bill_event', { 'payby' => $self->payby,
1158 'disabled' => '', } )
1161 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1163 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1165 my $cust_main = $self; #for callback
1166 my $error = eval $part_bill_event->eventcode;
1169 my $statustext = '';
1173 } elsif ( $error ) {
1175 $statustext = $error;
1180 #add cust_bill_event
1181 my $cust_bill_event = new FS::cust_bill_event {
1182 'invnum' => $cust_bill->invnum,
1183 'eventpart' => $part_bill_event->eventpart,
1184 '_date' => $invoice_time,
1185 'status' => $status,
1186 'statustext' => $statustext,
1188 $error = $cust_bill_event->insert;
1190 #$dbh->rollback if $oldAutoCommit;
1191 #return "error: $error";
1193 # gah, even with transactions.
1194 $dbh->commit if $oldAutoCommit; #well.
1195 my $e = 'WARNING: Event run but database not updated - '.
1196 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1197 ', eventpart '. $part_bill_event->eventpart.
1208 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1215 Returns the total owed for this customer on all invoices
1216 (see L<FS::cust_bill/owed>).
1222 $self->total_owed_date(2145859200); #12/31/2037
1225 =item total_owed_date TIME
1227 Returns the total owed for this customer on all invoices with date earlier than
1228 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1229 see L<Time::Local> and L<Date::Parse> for conversion functions.
1233 sub total_owed_date {
1237 foreach my $cust_bill (
1238 grep { $_->_date <= $time }
1239 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1241 $total_bill += $cust_bill->owed;
1243 sprintf( "%.2f", $total_bill );
1248 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1249 to outstanding invoice balances in chronological order and returns the value
1250 of any remaining unapplied credits available for refund
1251 (see L<FS::cust_refund>).
1258 return 0 unless $self->total_credited;
1260 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1261 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1263 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1264 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1268 foreach my $cust_bill ( @invoices ) {
1271 if ( !defined($credit) || $credit->credited == 0) {
1272 $credit = pop @credits or last;
1275 if ($cust_bill->owed >= $credit->credited) {
1276 $amount=$credit->credited;
1278 $amount=$cust_bill->owed;
1281 my $cust_credit_bill = new FS::cust_credit_bill ( {
1282 'crednum' => $credit->crednum,
1283 'invnum' => $cust_bill->invnum,
1284 'amount' => $amount,
1286 my $error = $cust_credit_bill->insert;
1287 die $error if $error;
1289 redo if ($cust_bill->owed > 0);
1293 return $self->total_credited;
1296 =item apply_payments
1298 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1299 to outstanding invoice balances in chronological order.
1301 #and returns the value of any remaining unapplied payments.
1305 sub apply_payments {
1310 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1311 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1313 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1314 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1318 foreach my $cust_bill ( @invoices ) {
1321 if ( !defined($payment) || $payment->unapplied == 0 ) {
1322 $payment = pop @payments or last;
1325 if ( $cust_bill->owed >= $payment->unapplied ) {
1326 $amount = $payment->unapplied;
1328 $amount = $cust_bill->owed;
1331 my $cust_bill_pay = new FS::cust_bill_pay ( {
1332 'paynum' => $payment->paynum,
1333 'invnum' => $cust_bill->invnum,
1334 'amount' => $amount,
1336 my $error = $cust_bill_pay->insert;
1337 die $error if $error;
1339 redo if ( $cust_bill->owed > 0);
1343 return $self->total_unapplied_payments;
1346 =item total_credited
1348 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1349 customer. See L<FS::cust_credit/credited>.
1353 sub total_credited {
1355 my $total_credit = 0;
1356 foreach my $cust_credit ( qsearch('cust_credit', {
1357 'custnum' => $self->custnum,
1359 $total_credit += $cust_credit->credited;
1361 sprintf( "%.2f", $total_credit );
1364 =item total_unapplied_payments
1366 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1367 See L<FS::cust_pay/unapplied>.
1371 sub total_unapplied_payments {
1373 my $total_unapplied = 0;
1374 foreach my $cust_pay ( qsearch('cust_pay', {
1375 'custnum' => $self->custnum,
1377 $total_unapplied += $cust_pay->unapplied;
1379 sprintf( "%.2f", $total_unapplied );
1384 Returns the balance for this customer (total_owed minus total_credited
1385 minus total_unapplied_payments).
1392 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1396 =item balance_date TIME
1398 Returns the balance for this customer, only considering invoices with date
1399 earlier than TIME (total_owed_date minus total_credited minus
1400 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1401 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1410 $self->total_owed_date($time)
1411 - $self->total_credited
1412 - $self->total_unapplied_payments
1416 =item invoicing_list [ ARRAYREF ]
1418 If an arguement is given, sets these email addresses as invoice recipients
1419 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1420 (except as warnings), so use check_invoicing_list first.
1422 Returns a list of email addresses (with svcnum entries expanded).
1424 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1425 check it without disturbing anything by passing nothing.
1427 This interface may change in the future.
1431 sub invoicing_list {
1432 my( $self, $arrayref ) = @_;
1434 my @cust_main_invoice;
1435 if ( $self->custnum ) {
1436 @cust_main_invoice =
1437 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1439 @cust_main_invoice = ();
1441 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1442 #warn $cust_main_invoice->destnum;
1443 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1444 #warn $cust_main_invoice->destnum;
1445 my $error = $cust_main_invoice->delete;
1446 warn $error if $error;
1449 if ( $self->custnum ) {
1450 @cust_main_invoice =
1451 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1453 @cust_main_invoice = ();
1455 my %seen = map { $_->address => 1 } @cust_main_invoice;
1456 foreach my $address ( @{$arrayref} ) {
1457 #unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1458 next if exists $seen{$address} && $seen{$address};
1459 $seen{$address} = 1;
1460 my $cust_main_invoice = new FS::cust_main_invoice ( {
1461 'custnum' => $self->custnum,
1464 my $error = $cust_main_invoice->insert;
1465 warn $error if $error;
1468 if ( $self->custnum ) {
1470 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1476 =item check_invoicing_list ARRAYREF
1478 Checks these arguements as valid input for the invoicing_list method. If there
1479 is an error, returns the error, otherwise returns false.
1483 sub check_invoicing_list {
1484 my( $self, $arrayref ) = @_;
1485 foreach my $address ( @{$arrayref} ) {
1486 my $cust_main_invoice = new FS::cust_main_invoice ( {
1487 'custnum' => $self->custnum,
1490 my $error = $self->custnum
1491 ? $cust_main_invoice->check
1492 : $cust_main_invoice->checkdest
1494 return $error if $error;
1499 =item default_invoicing_list
1501 Sets the invoicing list to all accounts associated with this customer.
1505 sub default_invoicing_list {
1508 foreach my $cust_pkg ( $self->all_pkgs ) {
1509 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1511 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1512 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1514 push @list, map { $_->email } @svc_acct;
1516 $self->invoicing_list(\@list);
1519 =item invoicing_list_addpost
1521 Adds postal invoicing to this customer. If this customer is already configured
1522 to receive postal invoices, does nothing.
1526 sub invoicing_list_addpost {
1528 return if grep { $_ eq 'POST' } $self->invoicing_list;
1529 my @invoicing_list = $self->invoicing_list;
1530 push @invoicing_list, 'POST';
1531 $self->invoicing_list(\@invoicing_list);
1534 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1536 Returns an array of customers referred by this customer (referral_custnum set
1537 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1538 customers referred by customers referred by this customer and so on, inclusive.
1539 The default behavior is DEPTH 1 (no recursion).
1543 sub referral_cust_main {
1545 my $depth = @_ ? shift : 1;
1546 my $exclude = @_ ? shift : {};
1549 map { $exclude->{$_->custnum}++; $_; }
1550 grep { ! $exclude->{ $_->custnum } }
1551 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1555 map { $_->referral_cust_main($depth-1, $exclude) }
1562 =item referral_cust_main_ncancelled
1564 Same as referral_cust_main, except only returns customers with uncancelled
1569 sub referral_cust_main_ncancelled {
1571 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
1574 =item referral_cust_pkg [ DEPTH ]
1576 Like referral_cust_main, except returns a flat list of all unsuspended (and
1577 uncancelled) packages for each customer. The number of items in this list may
1578 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
1582 sub referral_cust_pkg {
1584 my $depth = @_ ? shift : 1;
1586 map { $_->unsuspended_pkgs }
1587 grep { $_->unsuspended_pkgs }
1588 $self->referral_cust_main($depth);
1591 =item credit AMOUNT, REASON
1593 Applies a credit to this customer. If there is an error, returns the error,
1594 otherwise returns false.
1599 my( $self, $amount, $reason ) = @_;
1600 my $cust_credit = new FS::cust_credit {
1601 'custnum' => $self->custnum,
1602 'amount' => $amount,
1603 'reason' => $reason,
1605 $cust_credit->insert;
1608 =item charge AMOUNT PKG COMMENT
1610 Creates a one-time charge for this customer. If there is an error, returns
1611 the error, otherwise returns false.
1616 my ( $self, $amount, $pkg, $comment ) = @_;
1618 my $part_pkg = new FS::part_pkg ( {
1619 'pkg' => $pkg || 'One-time charge',
1620 'comment' => $comment || '$'. sprintf("%.2f".$amount),
1637 =item check_and_rebuild_fuzzyfiles
1641 sub check_and_rebuild_fuzzyfiles {
1642 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1643 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1644 or &rebuild_fuzzyfiles;
1647 =item rebuild_fuzzyfiles
1651 sub rebuild_fuzzyfiles {
1653 use Fcntl qw(:flock);
1655 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1659 open(LASTLOCK,">>$dir/cust_main.last")
1660 or die "can't open $dir/cust_main.last: $!";
1661 flock(LASTLOCK,LOCK_EX)
1662 or die "can't lock $dir/cust_main.last: $!";
1664 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1666 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1667 if defined dbdef->table('cust_main')->column('ship_last');
1669 open (LASTCACHE,">$dir/cust_main.last.tmp")
1670 or die "can't open $dir/cust_main.last.tmp: $!";
1671 print LASTCACHE join("\n", @all_last), "\n";
1672 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1674 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1679 open(COMPANYLOCK,">>$dir/cust_main.company")
1680 or die "can't open $dir/cust_main.company: $!";
1681 flock(COMPANYLOCK,LOCK_EX)
1682 or die "can't lock $dir/cust_main.company: $!";
1684 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1686 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1687 if defined dbdef->table('cust_main')->column('ship_last');
1689 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1690 or die "can't open $dir/cust_main.company.tmp: $!";
1691 print COMPANYCACHE join("\n", @all_company), "\n";
1692 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1694 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1704 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1705 open(LASTCACHE,"<$dir/cust_main.last")
1706 or die "can't open $dir/cust_main.last: $!";
1707 my @array = map { chomp; $_; } <LASTCACHE>;
1717 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1718 open(COMPANYCACHE,"<$dir/cust_main.company")
1719 or die "can't open $dir/cust_main.last: $!";
1720 my @array = map { chomp; $_; } <COMPANYCACHE>;
1725 =item append_fuzzyfiles LASTNAME COMPANY
1729 sub append_fuzzyfiles {
1730 my( $last, $company ) = @_;
1732 &check_and_rebuild_fuzzyfiles;
1734 use Fcntl qw(:flock);
1736 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1740 open(LAST,">>$dir/cust_main.last")
1741 or die "can't open $dir/cust_main.last: $!";
1743 or die "can't lock $dir/cust_main.last: $!";
1745 print LAST "$last\n";
1748 or die "can't unlock $dir/cust_main.last: $!";
1754 open(COMPANY,">>$dir/cust_main.company")
1755 or die "can't open $dir/cust_main.company: $!";
1756 flock(COMPANY,LOCK_EX)
1757 or die "can't lock $dir/cust_main.company: $!";
1759 print COMPANY "$company\n";
1761 flock(COMPANY,LOCK_UN)
1762 or die "can't unlock $dir/cust_main.company: $!";
1776 The delete method should possibly take an FS::cust_main object reference
1777 instead of a scalar customer number.
1779 Bill and collect options should probably be passed as references instead of a
1782 There should probably be a configuration file with a list of allowed credit
1785 No multiple currency support (probably a larger project than just this module).
1789 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1790 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
1791 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.