4 use vars qw( @ISA $conf $Debug $import );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
9 eval "use Time::Local;";
10 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
11 if $] < 5.006 && !defined($Time::Local::VERSION);
12 eval "use Time::Local qw(timelocal timelocal_nocheck);";
16 use Business::CreditCard;
17 use FS::UID qw( getotaker dbh );
18 use FS::Record qw( qsearchs qsearch dbdef );
19 use FS::Misc qw( send_email );
22 use FS::cust_bill_pkg;
25 use FS::part_referral;
26 use FS::cust_main_county;
28 use FS::cust_main_invoice;
29 use FS::cust_credit_bill;
30 use FS::cust_bill_pay;
31 use FS::prepay_credit;
34 use FS::part_bill_event;
35 use FS::cust_bill_event;
36 use FS::cust_tax_exempt;
38 use FS::Msgcat qw(gettext);
40 @ISA = qw( FS::Record );
42 $realtime_bop_decline_quiet = 0;
49 #ask FS::UID to run this stuff for us later
50 #$FS::UID::callback{'FS::cust_main'} = sub {
51 install_callback FS::UID sub {
53 #yes, need it for stuff below (prolly should be cached)
58 my ( $hashref, $cache ) = @_;
59 if ( exists $hashref->{'pkgnum'} ) {
60 # #@{ $self->{'_pkgnum'} } = ();
61 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
62 $self->{'_pkgnum'} = $subcache;
63 #push @{ $self->{'_pkgnum'} },
64 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
70 FS::cust_main - Object methods for cust_main records
76 $record = new FS::cust_main \%hash;
77 $record = new FS::cust_main { 'column' => 'value' };
79 $error = $record->insert;
81 $error = $new_record->replace($old_record);
83 $error = $record->delete;
85 $error = $record->check;
87 @cust_pkg = $record->all_pkgs;
89 @cust_pkg = $record->ncancelled_pkgs;
91 @cust_pkg = $record->suspended_pkgs;
93 $error = $record->bill;
94 $error = $record->bill %options;
95 $error = $record->bill 'time' => $time;
97 $error = $record->collect;
98 $error = $record->collect %options;
99 $error = $record->collect 'invoice_time' => $time,
100 'batch_card' => 'yes',
101 'report_badcard' => 'yes',
106 An FS::cust_main object represents a customer. FS::cust_main inherits from
107 FS::Record. The following fields are currently supported:
111 =item custnum - primary key (assigned automatically for new customers)
113 =item agentnum - agent (see L<FS::agent>)
115 =item refnum - Advertising source (see L<FS::part_referral>)
121 =item ss - social security number (optional)
123 =item company - (optional)
127 =item address2 - (optional)
131 =item county - (optional, see L<FS::cust_main_county>)
133 =item state - (see L<FS::cust_main_county>)
137 =item country - (see L<FS::cust_main_county>)
139 =item daytime - phone (optional)
141 =item night - phone (optional)
143 =item fax - phone (optional)
145 =item ship_first - name
147 =item ship_last - name
149 =item ship_company - (optional)
153 =item ship_address2 - (optional)
157 =item ship_county - (optional, see L<FS::cust_main_county>)
159 =item ship_state - (see L<FS::cust_main_county>)
163 =item ship_country - (see L<FS::cust_main_county>)
165 =item ship_daytime - phone (optional)
167 =item ship_night - phone (optional)
169 =item ship_fax - phone (optional)
171 =item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>)
173 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
175 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
177 =item payname - name on card or billing name
179 =item tax - tax exempt, empty or `Y'
181 =item otaker - order taker (assigned automatically, see L<FS::UID>)
183 =item comments - comments (optional)
185 =item referral_custnum - referring customer number
195 Creates a new customer. To add the customer to the database, see L<"insert">.
197 Note that this stores the hash reference, not a distinct copy of the hash it
198 points to. You can ask the object for a copy with the I<hash> method.
202 sub table { 'cust_main'; }
204 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
206 Adds this customer to the database. If there is an error, returns the error,
207 otherwise returns false.
209 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
210 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
211 are inserted atomicly, or the transaction is rolled back. Passing an empty
212 hash reference is equivalent to not supplying this parameter. There should be
213 a better explanation of this, but until then, here's an example:
216 tie %hash, 'Tie::RefHash'; #this part is important
218 $cust_pkg => [ $svc_acct ],
221 $cust_main->insert( \%hash );
223 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
224 be set as the invoicing list (see L<"invoicing_list">). Errors return as
225 expected and rollback the entire transaction; it is not necessary to call
226 check_invoicing_list first. The invoicing_list is set after the records in the
227 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
228 invoicing_list destination to the newly-created svc_acct. Here's an example:
230 $cust_main->insert( {}, [ $email, 'POST' ] );
232 Currently available options are: I<noexport>
234 If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
235 (You can schedule them later with the B<reexport> method.)
241 my $cust_pkgs = @_ ? shift : {};
242 my $invoicing_list = @_ ? shift : '';
245 local $SIG{HUP} = 'IGNORE';
246 local $SIG{INT} = 'IGNORE';
247 local $SIG{QUIT} = 'IGNORE';
248 local $SIG{TERM} = 'IGNORE';
249 local $SIG{TSTP} = 'IGNORE';
250 local $SIG{PIPE} = 'IGNORE';
252 my $oldAutoCommit = $FS::UID::AutoCommit;
253 local $FS::UID::AutoCommit = 0;
258 if ( $self->payby eq 'PREPAY' ) {
259 $self->payby('BILL');
260 my $prepay_credit = qsearchs(
262 { 'identifier' => $self->payinfo },
266 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
267 unless $prepay_credit;
268 $amount = $prepay_credit->amount;
269 $seconds = $prepay_credit->seconds;
270 my $error = $prepay_credit->delete;
272 $dbh->rollback if $oldAutoCommit;
273 return "removing prepay_credit (transaction rolled back): $error";
277 my $error = $self->SUPER::insert;
279 $dbh->rollback if $oldAutoCommit;
280 #return "inserting cust_main record (transaction rolled back): $error";
285 if ( $invoicing_list ) {
286 $error = $self->check_invoicing_list( $invoicing_list );
288 $dbh->rollback if $oldAutoCommit;
289 return "checking invoicing_list (transaction rolled back): $error";
291 $self->invoicing_list( $invoicing_list );
295 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
296 $error = $self->order_pkgs($cust_pkgs, \$seconds);
298 $dbh->rollback if $oldAutoCommit;
303 $dbh->rollback if $oldAutoCommit;
304 return "No svc_acct record to apply pre-paid time";
308 my $cust_credit = new FS::cust_credit {
309 'custnum' => $self->custnum,
312 $error = $cust_credit->insert;
314 $dbh->rollback if $oldAutoCommit;
315 return "inserting credit (transaction rolled back): $error";
319 $error = $self->queue_fuzzyfiles_update;
321 $dbh->rollback if $oldAutoCommit;
322 return "updating fuzzy search cache: $error";
325 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
332 document me. like ->insert(%cust_pkg) on an existing record
338 my $cust_pkgs = shift;
341 local $SIG{HUP} = 'IGNORE';
342 local $SIG{INT} = 'IGNORE';
343 local $SIG{QUIT} = 'IGNORE';
344 local $SIG{TERM} = 'IGNORE';
345 local $SIG{TSTP} = 'IGNORE';
346 local $SIG{PIPE} = 'IGNORE';
348 my $oldAutoCommit = $FS::UID::AutoCommit;
349 local $FS::UID::AutoCommit = 0;
352 foreach my $cust_pkg ( keys %$cust_pkgs ) {
353 $cust_pkg->custnum( $self->custnum );
354 my $error = $cust_pkg->insert;
356 $dbh->rollback if $oldAutoCommit;
357 return "inserting cust_pkg (transaction rolled back): $error";
359 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
360 $svc_something->pkgnum( $cust_pkg->pkgnum );
361 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
362 $svc_something->seconds( $svc_something->seconds + $$seconds );
365 $error = $svc_something->insert;
367 $dbh->rollback if $oldAutoCommit;
368 #return "inserting svc_ (transaction rolled back): $error";
374 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
380 document me. Re-schedules all exports by calling the B<reexport> method
381 of all associated packages (see L<FS::cust_pkg>). If there is an error,
382 returns the error; otherwise returns false.
389 local $SIG{HUP} = 'IGNORE';
390 local $SIG{INT} = 'IGNORE';
391 local $SIG{QUIT} = 'IGNORE';
392 local $SIG{TERM} = 'IGNORE';
393 local $SIG{TSTP} = 'IGNORE';
394 local $SIG{PIPE} = 'IGNORE';
396 my $oldAutoCommit = $FS::UID::AutoCommit;
397 local $FS::UID::AutoCommit = 0;
400 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
401 my $error = $cust_pkg->reexport;
403 $dbh->rollback if $oldAutoCommit;
408 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
413 =item delete NEW_CUSTNUM
415 This deletes the customer. If there is an error, returns the error, otherwise
418 This will completely remove all traces of the customer record. This is not
419 what you want when a customer cancels service; for that, cancel all of the
420 customer's packages (see L</cancel>).
422 If the customer has any uncancelled packages, you need to pass a new (valid)
423 customer number for those packages to be transferred to. Cancelled packages
424 will be deleted. Did I mention that this is NOT what you want when a customer
425 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
427 You can't delete a customer with invoices (see L<FS::cust_bill>),
428 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
429 refunds (see L<FS::cust_refund>).
436 local $SIG{HUP} = 'IGNORE';
437 local $SIG{INT} = 'IGNORE';
438 local $SIG{QUIT} = 'IGNORE';
439 local $SIG{TERM} = 'IGNORE';
440 local $SIG{TSTP} = 'IGNORE';
441 local $SIG{PIPE} = 'IGNORE';
443 my $oldAutoCommit = $FS::UID::AutoCommit;
444 local $FS::UID::AutoCommit = 0;
447 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
448 $dbh->rollback if $oldAutoCommit;
449 return "Can't delete a customer with invoices";
451 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
452 $dbh->rollback if $oldAutoCommit;
453 return "Can't delete a customer with credits";
455 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
456 $dbh->rollback if $oldAutoCommit;
457 return "Can't delete a customer with payments";
459 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
460 $dbh->rollback if $oldAutoCommit;
461 return "Can't delete a customer with refunds";
464 my @cust_pkg = $self->ncancelled_pkgs;
466 my $new_custnum = shift;
467 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
468 $dbh->rollback if $oldAutoCommit;
469 return "Invalid new customer number: $new_custnum";
471 foreach my $cust_pkg ( @cust_pkg ) {
472 my %hash = $cust_pkg->hash;
473 $hash{'custnum'} = $new_custnum;
474 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
475 my $error = $new_cust_pkg->replace($cust_pkg);
477 $dbh->rollback if $oldAutoCommit;
482 my @cancelled_cust_pkg = $self->all_pkgs;
483 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
484 my $error = $cust_pkg->delete;
486 $dbh->rollback if $oldAutoCommit;
491 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
492 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
494 my $error = $cust_main_invoice->delete;
496 $dbh->rollback if $oldAutoCommit;
501 my $error = $self->SUPER::delete;
503 $dbh->rollback if $oldAutoCommit;
507 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
512 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
514 Replaces the OLD_RECORD with this one in the database. If there is an error,
515 returns the error, otherwise returns false.
517 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
518 be set as the invoicing list (see L<"invoicing_list">). Errors return as
519 expected and rollback the entire transaction; it is not necessary to call
520 check_invoicing_list first. Here's an example:
522 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
531 local $SIG{HUP} = 'IGNORE';
532 local $SIG{INT} = 'IGNORE';
533 local $SIG{QUIT} = 'IGNORE';
534 local $SIG{TERM} = 'IGNORE';
535 local $SIG{TSTP} = 'IGNORE';
536 local $SIG{PIPE} = 'IGNORE';
538 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
539 && $conf->config('users-allow_comp') ) {
540 return "You are not permitted to create complimentary accounts."
541 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
544 my $oldAutoCommit = $FS::UID::AutoCommit;
545 local $FS::UID::AutoCommit = 0;
548 my $error = $self->SUPER::replace($old);
551 $dbh->rollback if $oldAutoCommit;
555 if ( @param ) { # INVOICING_LIST_ARYREF
556 my $invoicing_list = shift @param;
557 $error = $self->check_invoicing_list( $invoicing_list );
559 $dbh->rollback if $oldAutoCommit;
562 $self->invoicing_list( $invoicing_list );
565 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
566 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
567 # card/check/lec info has changed, want to retry realtime_ invoice events
568 my $error = $self->retry_realtime;
570 $dbh->rollback if $oldAutoCommit;
575 $error = $self->queue_fuzzyfiles_update;
577 $dbh->rollback if $oldAutoCommit;
578 return "updating fuzzy search cache: $error";
581 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
586 =item queue_fuzzyfiles_update
588 Used by insert & replace to update the fuzzy search cache
592 sub queue_fuzzyfiles_update {
595 local $SIG{HUP} = 'IGNORE';
596 local $SIG{INT} = 'IGNORE';
597 local $SIG{QUIT} = 'IGNORE';
598 local $SIG{TERM} = 'IGNORE';
599 local $SIG{TSTP} = 'IGNORE';
600 local $SIG{PIPE} = 'IGNORE';
602 my $oldAutoCommit = $FS::UID::AutoCommit;
603 local $FS::UID::AutoCommit = 0;
606 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
607 my $error = $queue->insert($self->getfield('last'), $self->company);
609 $dbh->rollback if $oldAutoCommit;
610 return "queueing job (transaction rolled back): $error";
613 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
614 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
615 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
617 $dbh->rollback if $oldAutoCommit;
618 return "queueing job (transaction rolled back): $error";
622 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
629 Checks all fields to make sure this is a valid customer record. If there is
630 an error, returns the error, otherwise returns false. Called by the insert
638 #warn "BEFORE: \n". $self->_dump;
641 $self->ut_numbern('custnum')
642 || $self->ut_number('agentnum')
643 || $self->ut_number('refnum')
644 || $self->ut_name('last')
645 || $self->ut_name('first')
646 || $self->ut_textn('company')
647 || $self->ut_text('address1')
648 || $self->ut_textn('address2')
649 || $self->ut_text('city')
650 || $self->ut_textn('county')
651 || $self->ut_textn('state')
652 || $self->ut_country('country')
653 || $self->ut_anything('comments')
654 || $self->ut_numbern('referral_custnum')
656 #barf. need message catalogs. i18n. etc.
657 $error .= "Please select an advertising source."
658 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
659 return $error if $error;
661 return "Unknown agent"
662 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
664 return "Unknown refnum"
665 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
667 return "Unknown referring custnum ". $self->referral_custnum
668 unless ! $self->referral_custnum
669 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
671 if ( $self->ss eq '' ) {
676 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
677 or return "Illegal social security number: ". $self->ss;
678 $self->ss("$1-$2-$3");
682 # bad idea to disable, causes billing to fail because of no tax rates later
683 # unless ( $import ) {
684 unless ( qsearch('cust_main_county', {
685 'country' => $self->country,
688 return "Unknown state/county/country: ".
689 $self->state. "/". $self->county. "/". $self->country
690 unless qsearch('cust_main_county',{
691 'state' => $self->state,
692 'county' => $self->county,
693 'country' => $self->country,
699 $self->ut_phonen('daytime', $self->country)
700 || $self->ut_phonen('night', $self->country)
701 || $self->ut_phonen('fax', $self->country)
702 || $self->ut_zip('zip', $self->country)
704 return $error if $error;
707 last first company address1 address2 city county state zip
708 country daytime night fax
711 if ( defined $self->dbdef_table->column('ship_last') ) {
712 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
714 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
718 $self->ut_name('ship_last')
719 || $self->ut_name('ship_first')
720 || $self->ut_textn('ship_company')
721 || $self->ut_text('ship_address1')
722 || $self->ut_textn('ship_address2')
723 || $self->ut_text('ship_city')
724 || $self->ut_textn('ship_county')
725 || $self->ut_textn('ship_state')
726 || $self->ut_country('ship_country')
728 return $error if $error;
730 #false laziness with above
731 unless ( qsearchs('cust_main_county', {
732 'country' => $self->ship_country,
735 return "Unknown ship_state/ship_county/ship_country: ".
736 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
737 unless qsearchs('cust_main_county',{
738 'state' => $self->ship_state,
739 'county' => $self->ship_county,
740 'country' => $self->ship_country,
746 $self->ut_phonen('ship_daytime', $self->ship_country)
747 || $self->ut_phonen('ship_night', $self->ship_country)
748 || $self->ut_phonen('ship_fax', $self->ship_country)
749 || $self->ut_zip('ship_zip', $self->ship_country)
751 return $error if $error;
753 } else { # ship_ info eq billing info, so don't store dup info in database
754 $self->setfield("ship_$_", '')
755 foreach qw( last first company address1 address2 city county state zip
756 country daytime night fax );
760 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
761 or return "Illegal payby: ". $self->payby;
764 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
766 my $payinfo = $self->payinfo;
768 $payinfo =~ /^(\d{13,16})$/
769 or return gettext('invalid_card'); # . ": ". $self->payinfo;
771 $self->payinfo($payinfo);
773 or return gettext('invalid_card'); # . ": ". $self->payinfo;
774 return gettext('unknown_card_type')
775 if cardtype($self->payinfo) eq "Unknown";
777 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
779 my $payinfo = $self->payinfo;
780 $payinfo =~ s/[^\d\@]//g;
781 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
783 $self->payinfo($payinfo);
785 } elsif ( $self->payby eq 'LECB' ) {
787 my $payinfo = $self->payinfo;
789 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
791 $self->payinfo($payinfo);
793 } elsif ( $self->payby eq 'BILL' ) {
795 $error = $self->ut_textn('payinfo');
796 return "Illegal P.O. number: ". $self->payinfo if $error;
798 } elsif ( $self->payby eq 'COMP' ) {
800 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
801 return "You are not permitted to create complimentary accounts."
802 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
805 $error = $self->ut_textn('payinfo');
806 return "Illegal comp account issuer: ". $self->payinfo if $error;
808 } elsif ( $self->payby eq 'PREPAY' ) {
810 my $payinfo = $self->payinfo;
811 $payinfo =~ s/\W//g; #anything else would just confuse things
812 $self->payinfo($payinfo);
813 $error = $self->ut_alpha('payinfo');
814 return "Illegal prepayment identifier: ". $self->payinfo if $error;
815 return "Unknown prepayment identifier"
816 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
820 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
821 return "Expriation date required"
822 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
826 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
827 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
828 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) {
829 ( $m, $y ) = ( $3, "20$2" );
831 return "Illegal expiration date: ". $self->paydate;
833 $self->paydate("$y-$m-01");
834 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
835 return gettext('expired_card')
836 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
839 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
840 ( ! $conf->exists('require_cardname')
841 || $self->payby !~ /^(CARD|DCRD)$/ )
843 $self->payname( $self->first. " ". $self->getfield('last') );
845 $self->payname =~ /^([\w \,\.\-\']+)$/
846 or return gettext('illegal_name'). " payname: ". $self->payname;
850 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
853 $self->otaker(getotaker);
855 #warn "AFTER: \n". $self->_dump;
862 Returns all packages (see L<FS::cust_pkg>) for this customer.
868 if ( $self->{'_pkgnum'} ) {
869 values %{ $self->{'_pkgnum'}->cache };
871 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
875 =item ncancelled_pkgs
877 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
881 sub ncancelled_pkgs {
883 if ( $self->{'_pkgnum'} ) {
884 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
886 @{ [ # force list context
887 qsearch( 'cust_pkg', {
888 'custnum' => $self->custnum,
891 qsearch( 'cust_pkg', {
892 'custnum' => $self->custnum,
901 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
907 grep { $_->susp } $self->ncancelled_pkgs;
910 =item unflagged_suspended_pkgs
912 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
913 customer (thouse packages without the `manual_flag' set).
917 sub unflagged_suspended_pkgs {
919 return $self->suspended_pkgs
920 unless dbdef->table('cust_pkg')->column('manual_flag');
921 grep { ! $_->manual_flag } $self->suspended_pkgs;
924 =item unsuspended_pkgs
926 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
931 sub unsuspended_pkgs {
933 grep { ! $_->susp } $self->ncancelled_pkgs;
938 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
939 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
940 on success or a list of errors.
946 grep { $_->unsuspend } $self->suspended_pkgs;
951 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
952 Always returns a list: an empty list on success or a list of errors.
958 grep { $_->suspend } $self->unsuspended_pkgs;
963 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
964 Always returns a list: an empty list on success or a list of errors.
970 grep { $_->cancel } $self->ncancelled_pkgs;
975 Returns the agent (see L<FS::agent>) for this customer.
981 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
986 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
987 conjunction with the collect method.
989 Options are passed as name-value pairs.
991 The only currently available option is `time', which bills the customer as if
992 it were that time. It is specified as a UNIX timestamp; see
993 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
994 functions. For example:
998 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1000 If there is an error, returns the error, otherwise returns false.
1005 my( $self, %options ) = @_;
1006 my $time = $options{'time'} || time;
1011 local $SIG{HUP} = 'IGNORE';
1012 local $SIG{INT} = 'IGNORE';
1013 local $SIG{QUIT} = 'IGNORE';
1014 local $SIG{TERM} = 'IGNORE';
1015 local $SIG{TSTP} = 'IGNORE';
1016 local $SIG{PIPE} = 'IGNORE';
1018 my $oldAutoCommit = $FS::UID::AutoCommit;
1019 local $FS::UID::AutoCommit = 0;
1022 # find the packages which are due for billing, find out how much they are
1023 # & generate invoice database.
1025 my( $total_setup, $total_recur ) = ( 0, 0 );
1026 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1027 my @cust_bill_pkg = ();
1029 #my $taxable_charged = 0;##
1034 foreach my $cust_pkg (
1035 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1038 #NO!! next if $cust_pkg->cancel;
1039 next if $cust_pkg->getfield('cancel');
1041 #? to avoid use of uninitialized value errors... ?
1042 $cust_pkg->setfield('bill', '')
1043 unless defined($cust_pkg->bill);
1045 my $part_pkg = $cust_pkg->part_pkg;
1047 #so we don't modify cust_pkg record unnecessarily
1048 my $cust_pkg_mod_flag = 0;
1049 my %hash = $cust_pkg->hash;
1050 my $old_cust_pkg = new FS::cust_pkg \%hash;
1056 unless ( $cust_pkg->setup ) {
1057 my $setup_prog = $part_pkg->getfield('setup');
1058 $setup_prog =~ /^(.*)$/ or do {
1059 $dbh->rollback if $oldAutoCommit;
1060 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
1064 $setup_prog = '0' if $setup_prog =~ /^\s*$/;
1066 #my $cpt = new Safe;
1067 ##$cpt->permit(); #what is necessary?
1068 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1069 #$setup = $cpt->reval($setup_prog);
1070 $setup = eval $setup_prog;
1071 unless ( defined($setup) ) {
1072 $dbh->rollback if $oldAutoCommit;
1073 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1074 "(expression $setup_prog): $@";
1076 $cust_pkg->setfield('setup',$time);
1077 $cust_pkg_mod_flag=1;
1083 if ( $part_pkg->getfield('freq') > 0 &&
1084 ! $cust_pkg->getfield('susp') &&
1085 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1087 my $recur_prog = $part_pkg->getfield('recur');
1088 $recur_prog =~ /^(.*)$/ or do {
1089 $dbh->rollback if $oldAutoCommit;
1090 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1094 $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1096 # shared with $recur_prog
1097 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1099 #my $cpt = new Safe;
1100 ##$cpt->permit(); #what is necessary?
1101 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1102 #$recur = $cpt->reval($recur_prog);
1103 $recur = eval $recur_prog;
1104 unless ( defined($recur) ) {
1105 $dbh->rollback if $oldAutoCommit;
1106 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1107 "(expression $recur_prog): $@";
1109 #change this bit to use Date::Manip? CAREFUL with timezones (see
1110 # mailing list archive)
1111 my ($sec,$min,$hour,$mday,$mon,$year) =
1112 (localtime($sdate) )[0,1,2,3,4,5];
1114 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1115 # only for figuring next bill date, nothing else, so, reset $sdate again
1117 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1118 $cust_pkg->last_bill($sdate)
1119 if $cust_pkg->dbdef_table->column('last_bill');
1121 $mon += $part_pkg->freq;
1122 until ( $mon < 12 ) { $mon -= 12; $year++; }
1123 $cust_pkg->setfield('bill',
1124 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1125 $cust_pkg_mod_flag = 1;
1128 warn "\$setup is undefined" unless defined($setup);
1129 warn "\$recur is undefined" unless defined($recur);
1130 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1132 my $taxable_charged = 0;
1133 if ( $cust_pkg_mod_flag ) {
1134 $error=$cust_pkg->replace($old_cust_pkg);
1135 if ( $error ) { #just in case
1136 $dbh->rollback if $oldAutoCommit;
1137 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1139 $setup = sprintf( "%.2f", $setup );
1140 $recur = sprintf( "%.2f", $recur );
1142 $dbh->rollback if $oldAutoCommit;
1143 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1146 $dbh->rollback if $oldAutoCommit;
1147 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1149 if ( $setup > 0 || $recur > 0 ) {
1150 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1151 'pkgnum' => $cust_pkg->pkgnum,
1155 'edate' => $cust_pkg->bill,
1156 'details' => \@details,
1158 push @cust_bill_pkg, $cust_bill_pkg;
1159 $total_setup += $setup;
1160 $total_recur += $recur;
1161 $taxable_charged += $setup
1162 unless $part_pkg->setuptax =~ /^Y$/i;
1163 $taxable_charged += $recur
1164 unless $part_pkg->recurtax =~ /^Y$/i;
1166 unless ( $self->tax =~ /Y/i
1167 || $self->payby eq 'COMP'
1168 || $taxable_charged == 0 ) {
1170 my $cust_main_county = qsearchs('cust_main_county',{
1171 'state' => $self->state,
1172 'county' => $self->county,
1173 'country' => $self->country,
1174 'taxclass' => $part_pkg->taxclass,
1176 $cust_main_county ||= qsearchs('cust_main_county',{
1177 'state' => $self->state,
1178 'county' => $self->county,
1179 'country' => $self->country,
1182 unless ( $cust_main_county ) {
1183 $dbh->rollback if $oldAutoCommit;
1185 "fatal: can't find tax rate for state/county/country/taxclass ".
1186 join('/', ( map $self->$_(), qw(state county country) ),
1187 $part_pkg->taxclass ). "\n";
1190 if ( $cust_main_county->exempt_amount ) {
1191 my ($mon,$year) = (localtime($sdate) )[4,5];
1193 my $freq = $part_pkg->freq || 1;
1194 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1195 foreach my $which_month ( 1 .. $freq ) {
1197 'custnum' => $self->custnum,
1198 'taxnum' => $cust_main_county->taxnum,
1199 'year' => 1900+$year,
1202 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1203 until ( $mon < 13 ) { $mon -= 12; $year++; }
1204 my $cust_tax_exempt =
1205 qsearchs('cust_tax_exempt', \%hash)
1206 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1207 my $remaining_exemption = sprintf("%.2f",
1208 $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
1209 if ( $remaining_exemption > 0 ) {
1210 my $addl = $remaining_exemption > $taxable_per_month
1211 ? $taxable_per_month
1212 : $remaining_exemption;
1213 $taxable_charged -= $addl;
1214 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1215 $cust_tax_exempt->hash,
1216 'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1218 $error = $new_cust_tax_exempt->exemptnum
1219 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1220 : $new_cust_tax_exempt->insert;
1222 $dbh->rollback if $oldAutoCommit;
1223 return "fatal: can't update cust_tax_exempt: $error";
1226 } # if $remaining_exemption > 0
1228 } #foreach $which_month
1230 } #if $cust_main_county->exempt_amount
1232 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1234 #$tax += $taxable_charged * $cust_main_county->tax / 100
1235 $tax{ $cust_main_county->taxname || 'Tax' } +=
1236 $taxable_charged * $cust_main_county->tax / 100
1238 } #unless $self->tax =~ /Y/i
1239 # || $self->payby eq 'COMP'
1240 # || $taxable_charged == 0
1242 } #if $setup > 0 || $recur > 0
1244 } #if $cust_pkg_mod_flag
1246 } #foreach my $cust_pkg
1248 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1249 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1251 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1252 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1256 # unless ( $self->tax =~ /Y/i
1257 # || $self->payby eq 'COMP'
1258 # || $taxable_charged == 0 ) {
1259 # my $cust_main_county = qsearchs('cust_main_county',{
1260 # 'state' => $self->state,
1261 # 'county' => $self->county,
1262 # 'country' => $self->country,
1263 # } ) or die "fatal: can't find tax rate for state/county/country ".
1264 # $self->state. "/". $self->county. "/". $self->country. "\n";
1265 # my $tax = sprintf( "%.2f",
1266 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1269 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1270 my $tax = sprintf("%.2f", $tax{$taxname} );
1271 $charged = sprintf( "%.2f", $charged+$tax );
1273 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1279 'itemdesc' => $taxname,
1281 push @cust_bill_pkg, $cust_bill_pkg;
1285 my $cust_bill = new FS::cust_bill ( {
1286 'custnum' => $self->custnum,
1288 'charged' => $charged,
1290 $error = $cust_bill->insert;
1292 $dbh->rollback if $oldAutoCommit;
1293 return "can't create invoice for customer #". $self->custnum. ": $error";
1296 my $invnum = $cust_bill->invnum;
1298 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1300 $cust_bill_pkg->invnum($invnum);
1301 $error = $cust_bill_pkg->insert;
1303 $dbh->rollback if $oldAutoCommit;
1304 return "can't create invoice line item for customer #". $self->custnum.
1309 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1313 =item collect OPTIONS
1315 (Attempt to) collect money for this customer's outstanding invoices (see
1316 L<FS::cust_bill>). Usually used after the bill method.
1318 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1319 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1320 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1322 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1323 and the invoice events web interface.
1325 If there is an error, returns the error, otherwise returns false.
1327 Options are passed as name-value pairs.
1329 Currently available options are:
1331 invoice_time - Use this time when deciding when to print invoices and
1332 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>
1333 for conversion functions.
1335 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1338 retry_card - Deprecated alias for 'retry'
1340 batch_card - This option is deprecated. See the invoice events web interface
1341 to control whether cards are batched or run against a realtime gateway.
1343 report_badcard - This option is deprecated.
1345 force_print - This option is deprecated; see the invoice events web interface.
1347 quiet - set true to surpress email card/ACH decline notices.
1352 my( $self, %options ) = @_;
1353 my $invoice_time = $options{'invoice_time'} || time;
1356 local $SIG{HUP} = 'IGNORE';
1357 local $SIG{INT} = 'IGNORE';
1358 local $SIG{QUIT} = 'IGNORE';
1359 local $SIG{TERM} = 'IGNORE';
1360 local $SIG{TSTP} = 'IGNORE';
1361 local $SIG{PIPE} = 'IGNORE';
1363 my $oldAutoCommit = $FS::UID::AutoCommit;
1364 local $FS::UID::AutoCommit = 0;
1367 my $balance = $self->balance;
1368 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1369 unless ( $balance > 0 ) { #redundant?????
1370 $dbh->rollback if $oldAutoCommit; #hmm
1374 if ( exists($options{'retry_card'}) ) {
1375 carp 'retry_card option passed to collect is deprecated; use retry';
1376 $options{'retry'} ||= $options{'retry_card'};
1378 if ( exists($options{'retry'}) && $options{'retry'} ) {
1379 my $error = $self->retry_realtime;
1381 $dbh->rollback if $oldAutoCommit;
1386 foreach my $cust_bill ( $self->cust_bill ) {
1388 #this has to be before next's
1389 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1393 $balance = sprintf( "%.2f", $balance - $amount );
1395 next unless $cust_bill->owed > 0;
1397 # don't try to charge for the same invoice if it's already in a batch
1398 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1400 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1402 next unless $amount > 0;
1405 foreach my $part_bill_event (
1406 sort { $a->seconds <=> $b->seconds
1407 || $a->weight <=> $b->weight
1408 || $a->eventpart <=> $b->eventpart }
1409 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1410 && ! qsearchs( 'cust_bill_event', {
1411 'invnum' => $cust_bill->invnum,
1412 'eventpart' => $_->eventpart,
1416 qsearch('part_bill_event', { 'payby' => $self->payby,
1417 'disabled' => '', } )
1420 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1422 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1424 my $cust_main = $self; #for callback
1428 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1429 $error = eval $part_bill_event->eventcode;
1433 my $statustext = '';
1437 } elsif ( $error ) {
1439 $statustext = $error;
1444 #add cust_bill_event
1445 my $cust_bill_event = new FS::cust_bill_event {
1446 'invnum' => $cust_bill->invnum,
1447 'eventpart' => $part_bill_event->eventpart,
1448 #'_date' => $invoice_time,
1450 'status' => $status,
1451 'statustext' => $statustext,
1453 $error = $cust_bill_event->insert;
1455 #$dbh->rollback if $oldAutoCommit;
1456 #return "error: $error";
1458 # gah, even with transactions.
1459 $dbh->commit if $oldAutoCommit; #well.
1460 my $e = 'WARNING: Event run but database not updated - '.
1461 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1462 ', eventpart '. $part_bill_event->eventpart.
1473 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1478 =item retry_realtime
1480 Schedules realtime credit card / electronic check / LEC billing events for
1481 for retry. Useful if card information has changed or manual retry is desired.
1482 The 'collect' method must be called to actually retry the transaction.
1484 Implementation details: For each of this customer's open invoices, changes
1485 the status of the first "done" (with statustext error) realtime processing
1490 sub retry_realtime {
1493 local $SIG{HUP} = 'IGNORE';
1494 local $SIG{INT} = 'IGNORE';
1495 local $SIG{QUIT} = 'IGNORE';
1496 local $SIG{TERM} = 'IGNORE';
1497 local $SIG{TSTP} = 'IGNORE';
1498 local $SIG{PIPE} = 'IGNORE';
1500 my $oldAutoCommit = $FS::UID::AutoCommit;
1501 local $FS::UID::AutoCommit = 0;
1504 foreach my $cust_bill (
1505 grep { $_->cust_bill_event }
1506 $self->open_cust_bill
1508 my @cust_bill_event =
1509 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1511 #$_->part_bill_event->plan eq 'realtime-card'
1512 $_->part_bill_event->eventcode =~
1513 /\$cust_bill\->realtime_(card|ach|lec)/
1514 && $_->status eq 'done'
1517 $cust_bill->cust_bill_event;
1518 next unless @cust_bill_event;
1519 my $error = $cust_bill_event[0]->retry;
1521 $dbh->rollback if $oldAutoCommit;
1522 return "error scheduling invoice event for retry: $error";
1527 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1532 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1534 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1535 via a Business::OnlinePayment realtime gateway. See
1536 L<http://420.am/business-onlinepayment> for supported gateways.
1538 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1540 Available options are: I<description>, I<invnum>, I<quiet>
1542 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1543 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1544 if set, will override the value from the customer record.
1546 I<description> is a free-text field passed to the gateway. It defaults to
1547 "Internet services".
1549 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1550 specified invoice. If you don't specify an I<invnum> you might want to
1551 call the B<apply_payments> method.
1553 I<quiet> can be set true to surpress email decline notices.
1555 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1560 my( $self, $method, $amount, %options ) = @_;
1562 warn "$self $method $amount\n";
1563 warn " $_ => $options{$_}\n" foreach keys %options;
1566 $options{'description'} ||= 'Internet services';
1569 die "Real-time processing not enabled\n"
1570 unless $conf->exists('business-onlinepayment');
1571 eval "use Business::OnlinePayment";
1575 $self->set( $_ => $options{$_} )
1576 foreach grep { exists($options{$_}) }
1577 qw( payname address1 address2 city state zip payinfo paydate );
1580 my $bop_config = 'business-onlinepayment';
1581 $bop_config .= '-ach'
1582 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1583 my ( $processor, $login, $password, $action, @bop_options ) =
1584 $conf->config($bop_config);
1585 $action ||= 'normal authorization';
1586 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1590 my $address = $self->address1;
1591 $address .= ", ". $self->address2 if $self->address2;
1593 my($payname, $payfirst, $paylast);
1594 if ( $self->payname && $method ne 'ECHECK' ) {
1595 $payname = $self->payname;
1596 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1597 or return "Illegal payname $payname";
1598 ($payfirst, $paylast) = ($1, $2);
1600 $payfirst = $self->getfield('first');
1601 $paylast = $self->getfield('last');
1602 $payname = "$payfirst $paylast";
1605 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1606 if ( $conf->exists('emailinvoiceauto')
1607 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1608 push @invoicing_list, $self->all_emails;
1610 my $email = $invoicing_list[0];
1613 if ( $method eq 'CC' ) {
1614 $content{card_number} = $self->payinfo;
1615 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1616 $content{expiration} = "$2/$1";
1617 } elsif ( $method eq 'ECHECK' ) {
1618 my($account_number,$routing_code) = $self->payinfo;
1619 ( $content{account_number}, $content{routing_code} ) =
1620 split('@', $self->payinfo);
1621 $content{bank_name} = $self->payname;
1622 $content{account_type} = 'CHECKING';
1623 $content{account_name} = $payname;
1624 $content{customer_org} = $self->company ? 'B' : 'I';
1625 $content{customer_ssn} = $self->ss;
1626 } elsif ( $method eq 'LEC' ) {
1627 $content{phone} = $self->payinfo;
1632 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1635 new Business::OnlinePayment( $processor, @bop_options );
1636 $transaction->content(
1639 'password' => $password,
1640 'action' => $action1,
1641 'description' => $options{'description'},
1642 'amount' => $amount,
1643 'invoice_number' => $options{'invnum'},
1644 'customer_id' => $self->custnum,
1645 'last_name' => $paylast,
1646 'first_name' => $payfirst,
1648 'address' => $address,
1649 'city' => $self->city,
1650 'state' => $self->state,
1651 'zip' => $self->zip,
1652 'country' => $self->country,
1653 'referer' => 'http://cleanwhisker.420.am/',
1655 'phone' => $self->daytime || $self->night,
1658 $transaction->submit();
1660 if ( $transaction->is_success() && $action2 ) {
1661 my $auth = $transaction->authorization;
1662 my $ordernum = $transaction->can('order_number')
1663 ? $transaction->order_number
1667 new Business::OnlinePayment( $processor, @bop_options );
1674 password => $password,
1675 order_number => $ordernum,
1677 authorization => $auth,
1678 description => $options{'description'},
1681 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1682 transaction_sequence_num local_transaction_date
1683 local_transaction_time AVS_result_code )) {
1684 $capture{$field} = $transaction->$field() if $transaction->can($field);
1687 $capture->content( %capture );
1691 unless ( $capture->is_success ) {
1692 my $e = "Authorization sucessful but capture failed, custnum #".
1693 $self->custnum. ': '. $capture->result_code.
1694 ": ". $capture->error_message;
1702 if ( $transaction->is_success() ) {
1704 my %method2payby = (
1710 my $cust_pay = new FS::cust_pay ( {
1711 'custnum' => $self->custnum,
1712 'invnum' => $options{'invnum'},
1715 'payby' => $method2payby{$method},
1716 'payinfo' => $self->payinfo,
1717 'paybatch' => "$processor:". $transaction->authorization,
1719 my $error = $cust_pay->insert;
1721 # gah, even with transactions.
1722 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1723 'error applying payment, invnum #' . $self->invnum.
1724 " ($processor): $error";
1733 my $perror = "$processor error: ". $transaction->error_message;
1735 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1736 && $conf->exists('emaildecline')
1737 && grep { $_ ne 'POST' } $self->invoicing_list
1739 my @templ = $conf->config('declinetemplate');
1740 my $template = new Text::Template (
1742 SOURCE => [ map "$_\n", @templ ],
1743 ) or return "($perror) can't create template: $Text::Template::ERROR";
1744 $template->compile()
1745 or return "($perror) can't compile template: $Text::Template::ERROR";
1747 my $templ_hash = { error => $transaction->error_message };
1749 my $error = send_email(
1750 'from' => $conf->config('invoice_from'),
1751 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1752 'subject' => 'Your payment could not be processed',
1753 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1756 $perror .= " (also received error sending decline notification: $error)"
1768 Returns the total owed for this customer on all invoices
1769 (see L<FS::cust_bill/owed>).
1775 $self->total_owed_date(2145859200); #12/31/2037
1778 =item total_owed_date TIME
1780 Returns the total owed for this customer on all invoices with date earlier than
1781 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1782 see L<Time::Local> and L<Date::Parse> for conversion functions.
1786 sub total_owed_date {
1790 foreach my $cust_bill (
1791 grep { $_->_date <= $time }
1792 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1794 $total_bill += $cust_bill->owed;
1796 sprintf( "%.2f", $total_bill );
1801 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1802 to outstanding invoice balances in chronological order and returns the value
1803 of any remaining unapplied credits available for refund
1804 (see L<FS::cust_refund>).
1811 return 0 unless $self->total_credited;
1813 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1814 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1816 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1817 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1821 foreach my $cust_bill ( @invoices ) {
1824 if ( !defined($credit) || $credit->credited == 0) {
1825 $credit = pop @credits or last;
1828 if ($cust_bill->owed >= $credit->credited) {
1829 $amount=$credit->credited;
1831 $amount=$cust_bill->owed;
1834 my $cust_credit_bill = new FS::cust_credit_bill ( {
1835 'crednum' => $credit->crednum,
1836 'invnum' => $cust_bill->invnum,
1837 'amount' => $amount,
1839 my $error = $cust_credit_bill->insert;
1840 die $error if $error;
1842 redo if ($cust_bill->owed > 0);
1846 return $self->total_credited;
1849 =item apply_payments
1851 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1852 to outstanding invoice balances in chronological order.
1854 #and returns the value of any remaining unapplied payments.
1858 sub apply_payments {
1863 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1864 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1866 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1867 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1871 foreach my $cust_bill ( @invoices ) {
1874 if ( !defined($payment) || $payment->unapplied == 0 ) {
1875 $payment = pop @payments or last;
1878 if ( $cust_bill->owed >= $payment->unapplied ) {
1879 $amount = $payment->unapplied;
1881 $amount = $cust_bill->owed;
1884 my $cust_bill_pay = new FS::cust_bill_pay ( {
1885 'paynum' => $payment->paynum,
1886 'invnum' => $cust_bill->invnum,
1887 'amount' => $amount,
1889 my $error = $cust_bill_pay->insert;
1890 die $error if $error;
1892 redo if ( $cust_bill->owed > 0);
1896 return $self->total_unapplied_payments;
1899 =item total_credited
1901 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1902 customer. See L<FS::cust_credit/credited>.
1906 sub total_credited {
1908 my $total_credit = 0;
1909 foreach my $cust_credit ( qsearch('cust_credit', {
1910 'custnum' => $self->custnum,
1912 $total_credit += $cust_credit->credited;
1914 sprintf( "%.2f", $total_credit );
1917 =item total_unapplied_payments
1919 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1920 See L<FS::cust_pay/unapplied>.
1924 sub total_unapplied_payments {
1926 my $total_unapplied = 0;
1927 foreach my $cust_pay ( qsearch('cust_pay', {
1928 'custnum' => $self->custnum,
1930 $total_unapplied += $cust_pay->unapplied;
1932 sprintf( "%.2f", $total_unapplied );
1937 Returns the balance for this customer (total_owed minus total_credited
1938 minus total_unapplied_payments).
1945 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1949 =item balance_date TIME
1951 Returns the balance for this customer, only considering invoices with date
1952 earlier than TIME (total_owed_date minus total_credited minus
1953 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1954 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1963 $self->total_owed_date($time)
1964 - $self->total_credited
1965 - $self->total_unapplied_payments
1969 =item invoicing_list [ ARRAYREF ]
1971 If an arguement is given, sets these email addresses as invoice recipients
1972 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1973 (except as warnings), so use check_invoicing_list first.
1975 Returns a list of email addresses (with svcnum entries expanded).
1977 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1978 check it without disturbing anything by passing nothing.
1980 This interface may change in the future.
1984 sub invoicing_list {
1985 my( $self, $arrayref ) = @_;
1987 my @cust_main_invoice;
1988 if ( $self->custnum ) {
1989 @cust_main_invoice =
1990 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1992 @cust_main_invoice = ();
1994 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1995 #warn $cust_main_invoice->destnum;
1996 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1997 #warn $cust_main_invoice->destnum;
1998 my $error = $cust_main_invoice->delete;
1999 warn $error if $error;
2002 if ( $self->custnum ) {
2003 @cust_main_invoice =
2004 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2006 @cust_main_invoice = ();
2008 my %seen = map { $_->address => 1 } @cust_main_invoice;
2009 foreach my $address ( @{$arrayref} ) {
2010 next if exists $seen{$address} && $seen{$address};
2011 $seen{$address} = 1;
2012 my $cust_main_invoice = new FS::cust_main_invoice ( {
2013 'custnum' => $self->custnum,
2016 my $error = $cust_main_invoice->insert;
2017 warn $error if $error;
2020 if ( $self->custnum ) {
2022 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2028 =item check_invoicing_list ARRAYREF
2030 Checks these arguements as valid input for the invoicing_list method. If there
2031 is an error, returns the error, otherwise returns false.
2035 sub check_invoicing_list {
2036 my( $self, $arrayref ) = @_;
2037 foreach my $address ( @{$arrayref} ) {
2038 my $cust_main_invoice = new FS::cust_main_invoice ( {
2039 'custnum' => $self->custnum,
2042 my $error = $self->custnum
2043 ? $cust_main_invoice->check
2044 : $cust_main_invoice->checkdest
2046 return $error if $error;
2051 =item set_default_invoicing_list
2053 Sets the invoicing list to all accounts associated with this customer,
2054 overwriting any previous invoicing list.
2058 sub set_default_invoicing_list {
2060 $self->invoicing_list($self->all_emails);
2065 Returns the email addresses of all accounts provisioned for this customer.
2072 foreach my $cust_pkg ( $self->all_pkgs ) {
2073 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2075 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2076 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2078 $list{$_}=1 foreach map { $_->email } @svc_acct;
2083 =item invoicing_list_addpost
2085 Adds postal invoicing to this customer. If this customer is already configured
2086 to receive postal invoices, does nothing.
2090 sub invoicing_list_addpost {
2092 return if grep { $_ eq 'POST' } $self->invoicing_list;
2093 my @invoicing_list = $self->invoicing_list;
2094 push @invoicing_list, 'POST';
2095 $self->invoicing_list(\@invoicing_list);
2098 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2100 Returns an array of customers referred by this customer (referral_custnum set
2101 to this custnum). If DEPTH is given, recurses up to the given depth, returning
2102 customers referred by customers referred by this customer and so on, inclusive.
2103 The default behavior is DEPTH 1 (no recursion).
2107 sub referral_cust_main {
2109 my $depth = @_ ? shift : 1;
2110 my $exclude = @_ ? shift : {};
2113 map { $exclude->{$_->custnum}++; $_; }
2114 grep { ! $exclude->{ $_->custnum } }
2115 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2119 map { $_->referral_cust_main($depth-1, $exclude) }
2126 =item referral_cust_main_ncancelled
2128 Same as referral_cust_main, except only returns customers with uncancelled
2133 sub referral_cust_main_ncancelled {
2135 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2138 =item referral_cust_pkg [ DEPTH ]
2140 Like referral_cust_main, except returns a flat list of all unsuspended (and
2141 uncancelled) packages for each customer. The number of items in this list may
2142 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2146 sub referral_cust_pkg {
2148 my $depth = @_ ? shift : 1;
2150 map { $_->unsuspended_pkgs }
2151 grep { $_->unsuspended_pkgs }
2152 $self->referral_cust_main($depth);
2155 =item credit AMOUNT, REASON
2157 Applies a credit to this customer. If there is an error, returns the error,
2158 otherwise returns false.
2163 my( $self, $amount, $reason ) = @_;
2164 my $cust_credit = new FS::cust_credit {
2165 'custnum' => $self->custnum,
2166 'amount' => $amount,
2167 'reason' => $reason,
2169 $cust_credit->insert;
2172 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2174 Creates a one-time charge for this customer. If there is an error, returns
2175 the error, otherwise returns false.
2180 my ( $self, $amount ) = ( shift, shift );
2181 my $pkg = @_ ? shift : 'One-time charge';
2182 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2183 my $taxclass = @_ ? shift : '';
2185 local $SIG{HUP} = 'IGNORE';
2186 local $SIG{INT} = 'IGNORE';
2187 local $SIG{QUIT} = 'IGNORE';
2188 local $SIG{TERM} = 'IGNORE';
2189 local $SIG{TSTP} = 'IGNORE';
2190 local $SIG{PIPE} = 'IGNORE';
2192 my $oldAutoCommit = $FS::UID::AutoCommit;
2193 local $FS::UID::AutoCommit = 0;
2196 my $part_pkg = new FS::part_pkg ( {
2198 'comment' => $comment,
2203 'taxclass' => $taxclass,
2206 my $error = $part_pkg->insert;
2208 $dbh->rollback if $oldAutoCommit;
2212 my $pkgpart = $part_pkg->pkgpart;
2213 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2214 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2215 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2216 $error = $type_pkgs->insert;
2218 $dbh->rollback if $oldAutoCommit;
2223 my $cust_pkg = new FS::cust_pkg ( {
2224 'custnum' => $self->custnum,
2225 'pkgpart' => $pkgpart,
2228 $error = $cust_pkg->insert;
2230 $dbh->rollback if $oldAutoCommit;
2234 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2241 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2247 sort { $a->_date <=> $b->_date }
2248 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2251 =item open_cust_bill
2253 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2258 sub open_cust_bill {
2260 grep { $_->owed > 0 } $self->cust_bill;
2269 =item check_and_rebuild_fuzzyfiles
2273 sub check_and_rebuild_fuzzyfiles {
2274 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2275 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2276 or &rebuild_fuzzyfiles;
2279 =item rebuild_fuzzyfiles
2283 sub rebuild_fuzzyfiles {
2285 use Fcntl qw(:flock);
2287 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2291 open(LASTLOCK,">>$dir/cust_main.last")
2292 or die "can't open $dir/cust_main.last: $!";
2293 flock(LASTLOCK,LOCK_EX)
2294 or die "can't lock $dir/cust_main.last: $!";
2296 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2298 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2299 if defined dbdef->table('cust_main')->column('ship_last');
2301 open (LASTCACHE,">$dir/cust_main.last.tmp")
2302 or die "can't open $dir/cust_main.last.tmp: $!";
2303 print LASTCACHE join("\n", @all_last), "\n";
2304 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2306 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
2311 open(COMPANYLOCK,">>$dir/cust_main.company")
2312 or die "can't open $dir/cust_main.company: $!";
2313 flock(COMPANYLOCK,LOCK_EX)
2314 or die "can't lock $dir/cust_main.company: $!";
2316 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
2318 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
2319 if defined dbdef->table('cust_main')->column('ship_last');
2321 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
2322 or die "can't open $dir/cust_main.company.tmp: $!";
2323 print COMPANYCACHE join("\n", @all_company), "\n";
2324 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
2326 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
2336 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2337 open(LASTCACHE,"<$dir/cust_main.last")
2338 or die "can't open $dir/cust_main.last: $!";
2339 my @array = map { chomp; $_; } <LASTCACHE>;
2349 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2350 open(COMPANYCACHE,"<$dir/cust_main.company")
2351 or die "can't open $dir/cust_main.last: $!";
2352 my @array = map { chomp; $_; } <COMPANYCACHE>;
2357 =item append_fuzzyfiles LASTNAME COMPANY
2361 sub append_fuzzyfiles {
2362 my( $last, $company ) = @_;
2364 &check_and_rebuild_fuzzyfiles;
2366 use Fcntl qw(:flock);
2368 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2372 open(LAST,">>$dir/cust_main.last")
2373 or die "can't open $dir/cust_main.last: $!";
2375 or die "can't lock $dir/cust_main.last: $!";
2377 print LAST "$last\n";
2380 or die "can't unlock $dir/cust_main.last: $!";
2386 open(COMPANY,">>$dir/cust_main.company")
2387 or die "can't open $dir/cust_main.company: $!";
2388 flock(COMPANY,LOCK_EX)
2389 or die "can't lock $dir/cust_main.company: $!";
2391 print COMPANY "$company\n";
2393 flock(COMPANY,LOCK_UN)
2394 or die "can't unlock $dir/cust_main.company: $!";
2408 #warn join('-',keys %$param);
2409 my $fh = $param->{filehandle};
2410 my $agentnum = $param->{agentnum};
2411 my $refnum = $param->{refnum};
2412 my $pkgpart = $param->{pkgpart};
2413 my @fields = @{$param->{fields}};
2415 eval "use Date::Parse;";
2417 eval "use Text::CSV_XS;";
2420 my $csv = new Text::CSV_XS;
2427 local $SIG{HUP} = 'IGNORE';
2428 local $SIG{INT} = 'IGNORE';
2429 local $SIG{QUIT} = 'IGNORE';
2430 local $SIG{TERM} = 'IGNORE';
2431 local $SIG{TSTP} = 'IGNORE';
2432 local $SIG{PIPE} = 'IGNORE';
2434 my $oldAutoCommit = $FS::UID::AutoCommit;
2435 local $FS::UID::AutoCommit = 0;
2438 #while ( $columns = $csv->getline($fh) ) {
2440 while ( defined($line=<$fh>) ) {
2442 $csv->parse($line) or do {
2443 $dbh->rollback if $oldAutoCommit;
2444 return "can't parse: ". $csv->error_input();
2447 my @columns = $csv->fields();
2448 #warn join('-',@columns);
2451 agentnum => $agentnum,
2453 country => 'US', #default
2454 payby => 'BILL', #default
2455 paydate => '12/2037', #default
2457 my $billtime = time;
2458 my %cust_pkg = ( pkgpart => $pkgpart );
2459 foreach my $field ( @fields ) {
2460 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2461 #$cust_pkg{$1} = str2time( shift @$columns );
2462 if ( $1 eq 'setup' ) {
2463 $billtime = str2time(shift @columns);
2465 $cust_pkg{$1} = str2time( shift @columns );
2468 #$cust_main{$field} = shift @$columns;
2469 $cust_main{$field} = shift @columns;
2473 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2474 my $cust_main = new FS::cust_main ( \%cust_main );
2476 tie my %hash, 'Tie::RefHash'; #this part is important
2477 $hash{$cust_pkg} = [] if $pkgpart;
2478 my $error = $cust_main->insert( \%hash );
2481 $dbh->rollback if $oldAutoCommit;
2482 return "can't insert customer for $line: $error";
2485 #false laziness w/bill.cgi
2486 $error = $cust_main->bill( 'time' => $billtime );
2488 $dbh->rollback if $oldAutoCommit;
2489 return "can't bill customer for $line: $error";
2492 $cust_main->apply_payments;
2493 $cust_main->apply_credits;
2495 $error = $cust_main->collect();
2497 $dbh->rollback if $oldAutoCommit;
2498 return "can't collect customer for $line: $error";
2504 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2506 return "Empty file!" unless $imported;
2518 #warn join('-',keys %$param);
2519 my $fh = $param->{filehandle};
2520 my @fields = @{$param->{fields}};
2522 eval "use Date::Parse;";
2524 eval "use Text::CSV_XS;";
2527 my $csv = new Text::CSV_XS;
2534 local $SIG{HUP} = 'IGNORE';
2535 local $SIG{INT} = 'IGNORE';
2536 local $SIG{QUIT} = 'IGNORE';
2537 local $SIG{TERM} = 'IGNORE';
2538 local $SIG{TSTP} = 'IGNORE';
2539 local $SIG{PIPE} = 'IGNORE';
2541 my $oldAutoCommit = $FS::UID::AutoCommit;
2542 local $FS::UID::AutoCommit = 0;
2545 #while ( $columns = $csv->getline($fh) ) {
2547 while ( defined($line=<$fh>) ) {
2549 $csv->parse($line) or do {
2550 $dbh->rollback if $oldAutoCommit;
2551 return "can't parse: ". $csv->error_input();
2554 my @columns = $csv->fields();
2555 #warn join('-',@columns);
2558 foreach my $field ( @fields ) {
2559 $row{$field} = shift @columns;
2562 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2563 unless ( $cust_main ) {
2564 $dbh->rollback if $oldAutoCommit;
2565 return "unknown custnum $row{'custnum'}";
2568 if ( $row{'amount'} > 0 ) {
2569 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2571 $dbh->rollback if $oldAutoCommit;
2575 } elsif ( $row{'amount'} < 0 ) {
2576 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2579 $dbh->rollback if $oldAutoCommit;
2589 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2591 return "Empty file!" unless $imported;
2603 The delete method should possibly take an FS::cust_main object reference
2604 instead of a scalar customer number.
2606 Bill and collect options should probably be passed as references instead of a
2609 There should probably be a configuration file with a list of allowed credit
2612 No multiple currency support (probably a larger project than just this module).
2616 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2617 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2618 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.