4 use vars qw( @ISA $conf $Debug $import );
7 use Time::Local qw(timelocal_nocheck);
10 use Business::CreditCard;
11 use FS::UID qw( getotaker dbh );
12 use FS::Record qw( qsearchs qsearch dbdef );
13 use FS::Misc qw( send_email );
16 use FS::cust_bill_pkg;
19 use FS::part_referral;
20 use FS::cust_main_county;
22 use FS::cust_main_invoice;
23 use FS::cust_credit_bill;
24 use FS::cust_bill_pay;
25 use FS::prepay_credit;
28 use FS::part_bill_event;
29 use FS::cust_bill_event;
30 use FS::cust_tax_exempt;
32 use FS::Msgcat qw(gettext);
34 @ISA = qw( FS::Record );
41 #ask FS::UID to run this stuff for us later
42 #$FS::UID::callback{'FS::cust_main'} = sub {
43 install_callback FS::UID sub {
45 #yes, need it for stuff below (prolly should be cached)
50 my ( $hashref, $cache ) = @_;
51 if ( exists $hashref->{'pkgnum'} ) {
52 # #@{ $self->{'_pkgnum'} } = ();
53 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
54 $self->{'_pkgnum'} = $subcache;
55 #push @{ $self->{'_pkgnum'} },
56 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
62 FS::cust_main - Object methods for cust_main records
68 $record = new FS::cust_main \%hash;
69 $record = new FS::cust_main { 'column' => 'value' };
71 $error = $record->insert;
73 $error = $new_record->replace($old_record);
75 $error = $record->delete;
77 $error = $record->check;
79 @cust_pkg = $record->all_pkgs;
81 @cust_pkg = $record->ncancelled_pkgs;
83 @cust_pkg = $record->suspended_pkgs;
85 $error = $record->bill;
86 $error = $record->bill %options;
87 $error = $record->bill 'time' => $time;
89 $error = $record->collect;
90 $error = $record->collect %options;
91 $error = $record->collect 'invoice_time' => $time,
92 'batch_card' => 'yes',
93 'report_badcard' => 'yes',
98 An FS::cust_main object represents a customer. FS::cust_main inherits from
99 FS::Record. The following fields are currently supported:
103 =item custnum - primary key (assigned automatically for new customers)
105 =item agentnum - agent (see L<FS::agent>)
107 =item refnum - Advertising source (see L<FS::part_referral>)
113 =item ss - social security number (optional)
115 =item company - (optional)
119 =item address2 - (optional)
123 =item county - (optional, see L<FS::cust_main_county>)
125 =item state - (see L<FS::cust_main_county>)
129 =item country - (see L<FS::cust_main_county>)
131 =item daytime - phone (optional)
133 =item night - phone (optional)
135 =item fax - phone (optional)
137 =item ship_first - name
139 =item ship_last - name
141 =item ship_company - (optional)
145 =item ship_address2 - (optional)
149 =item ship_county - (optional, see L<FS::cust_main_county>)
151 =item ship_state - (see L<FS::cust_main_county>)
155 =item ship_country - (see L<FS::cust_main_county>)
157 =item ship_daytime - phone (optional)
159 =item ship_night - phone (optional)
161 =item ship_fax - phone (optional)
163 =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>)
165 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
167 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
169 =item payname - name on card or billing name
171 =item tax - tax exempt, empty or `Y'
173 =item otaker - order taker (assigned automatically, see L<FS::UID>)
175 =item comments - comments (optional)
177 =item referral_custnum - referring customer number
187 Creates a new customer. To add the customer to the database, see L<"insert">.
189 Note that this stores the hash reference, not a distinct copy of the hash it
190 points to. You can ask the object for a copy with the I<hash> method.
194 sub table { 'cust_main'; }
196 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
198 Adds this customer to the database. If there is an error, returns the error,
199 otherwise returns false.
201 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
202 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
203 are inserted atomicly, or the transaction is rolled back. Passing an empty
204 hash reference is equivalent to not supplying this parameter. There should be
205 a better explanation of this, but until then, here's an example:
208 tie %hash, 'Tie::RefHash'; #this part is important
210 $cust_pkg => [ $svc_acct ],
213 $cust_main->insert( \%hash );
215 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
216 be set as the invoicing list (see L<"invoicing_list">). Errors return as
217 expected and rollback the entire transaction; it is not necessary to call
218 check_invoicing_list first. The invoicing_list is set after the records in the
219 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
220 invoicing_list destination to the newly-created svc_acct. Here's an example:
222 $cust_main->insert( {}, [ $email, 'POST' ] );
228 my $cust_pkgs = @_ ? shift : {};
229 my $invoicing_list = @_ ? shift : '';
231 local $SIG{HUP} = 'IGNORE';
232 local $SIG{INT} = 'IGNORE';
233 local $SIG{QUIT} = 'IGNORE';
234 local $SIG{TERM} = 'IGNORE';
235 local $SIG{TSTP} = 'IGNORE';
236 local $SIG{PIPE} = 'IGNORE';
238 my $oldAutoCommit = $FS::UID::AutoCommit;
239 local $FS::UID::AutoCommit = 0;
244 if ( $self->payby eq 'PREPAY' ) {
245 $self->payby('BILL');
246 my $prepay_credit = qsearchs(
248 { 'identifier' => $self->payinfo },
252 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
253 unless $prepay_credit;
254 $amount = $prepay_credit->amount;
255 $seconds = $prepay_credit->seconds;
256 my $error = $prepay_credit->delete;
258 $dbh->rollback if $oldAutoCommit;
259 return "removing prepay_credit (transaction rolled back): $error";
263 my $error = $self->SUPER::insert;
265 $dbh->rollback if $oldAutoCommit;
266 #return "inserting cust_main record (transaction rolled back): $error";
271 if ( $invoicing_list ) {
272 $error = $self->check_invoicing_list( $invoicing_list );
274 $dbh->rollback if $oldAutoCommit;
275 return "checking invoicing_list (transaction rolled back): $error";
277 $self->invoicing_list( $invoicing_list );
281 $error = $self->order_pkgs($cust_pkgs, \$seconds);
283 $dbh->rollback if $oldAutoCommit;
288 $dbh->rollback if $oldAutoCommit;
289 return "No svc_acct record to apply pre-paid time";
293 my $cust_credit = new FS::cust_credit {
294 'custnum' => $self->custnum,
297 $error = $cust_credit->insert;
299 $dbh->rollback if $oldAutoCommit;
300 return "inserting credit (transaction rolled back): $error";
304 #false laziness with sub replace
305 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
306 $error = $queue->insert($self->getfield('last'), $self->company);
308 $dbh->rollback if $oldAutoCommit;
309 return "queueing job (transaction rolled back): $error";
312 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
313 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
314 $error = $queue->insert($self->getfield('last'), $self->company);
316 $dbh->rollback if $oldAutoCommit;
317 return "queueing job (transaction rolled back): $error";
322 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
329 document me. like ->insert(%cust_pkg) on an existing record
335 my $cust_pkgs = shift;
338 local $SIG{HUP} = 'IGNORE';
339 local $SIG{INT} = 'IGNORE';
340 local $SIG{QUIT} = 'IGNORE';
341 local $SIG{TERM} = 'IGNORE';
342 local $SIG{TSTP} = 'IGNORE';
343 local $SIG{PIPE} = 'IGNORE';
345 my $oldAutoCommit = $FS::UID::AutoCommit;
346 local $FS::UID::AutoCommit = 0;
349 foreach my $cust_pkg ( keys %$cust_pkgs ) {
350 $cust_pkg->custnum( $self->custnum );
351 my $error = $cust_pkg->insert;
353 $dbh->rollback if $oldAutoCommit;
354 return "inserting cust_pkg (transaction rolled back): $error";
356 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
357 $svc_something->pkgnum( $cust_pkg->pkgnum );
358 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
359 $svc_something->seconds( $svc_something->seconds + $$seconds );
362 $error = $svc_something->insert;
364 $dbh->rollback if $oldAutoCommit;
365 #return "inserting svc_ (transaction rolled back): $error";
371 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
375 =item delete NEW_CUSTNUM
377 This deletes the customer. If there is an error, returns the error, otherwise
380 This will completely remove all traces of the customer record. This is not
381 what you want when a customer cancels service; for that, cancel all of the
382 customer's packages (see L<FS::cust_pkg/cancel>).
384 If the customer has any uncancelled packages, you need to pass a new (valid)
385 customer number for those packages to be transferred to. Cancelled packages
386 will be deleted. Did I mention that this is NOT what you want when a customer
387 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
389 You can't delete a customer with invoices (see L<FS::cust_bill>),
390 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
391 refunds (see L<FS::cust_refund>).
398 local $SIG{HUP} = 'IGNORE';
399 local $SIG{INT} = 'IGNORE';
400 local $SIG{QUIT} = 'IGNORE';
401 local $SIG{TERM} = 'IGNORE';
402 local $SIG{TSTP} = 'IGNORE';
403 local $SIG{PIPE} = 'IGNORE';
405 my $oldAutoCommit = $FS::UID::AutoCommit;
406 local $FS::UID::AutoCommit = 0;
409 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
410 $dbh->rollback if $oldAutoCommit;
411 return "Can't delete a customer with invoices";
413 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
414 $dbh->rollback if $oldAutoCommit;
415 return "Can't delete a customer with credits";
417 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
418 $dbh->rollback if $oldAutoCommit;
419 return "Can't delete a customer with payments";
421 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
422 $dbh->rollback if $oldAutoCommit;
423 return "Can't delete a customer with refunds";
426 my @cust_pkg = $self->ncancelled_pkgs;
428 my $new_custnum = shift;
429 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
430 $dbh->rollback if $oldAutoCommit;
431 return "Invalid new customer number: $new_custnum";
433 foreach my $cust_pkg ( @cust_pkg ) {
434 my %hash = $cust_pkg->hash;
435 $hash{'custnum'} = $new_custnum;
436 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
437 my $error = $new_cust_pkg->replace($cust_pkg);
439 $dbh->rollback if $oldAutoCommit;
444 my @cancelled_cust_pkg = $self->all_pkgs;
445 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
446 my $error = $cust_pkg->delete;
448 $dbh->rollback if $oldAutoCommit;
453 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
454 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
456 my $error = $cust_main_invoice->delete;
458 $dbh->rollback if $oldAutoCommit;
463 my $error = $self->SUPER::delete;
465 $dbh->rollback if $oldAutoCommit;
469 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
474 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
476 Replaces the OLD_RECORD with this one in the database. If there is an error,
477 returns the error, otherwise returns false.
479 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
480 be set as the invoicing list (see L<"invoicing_list">). Errors return as
481 expected and rollback the entire transaction; it is not necessary to call
482 check_invoicing_list first. Here's an example:
484 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
493 local $SIG{HUP} = 'IGNORE';
494 local $SIG{INT} = 'IGNORE';
495 local $SIG{QUIT} = 'IGNORE';
496 local $SIG{TERM} = 'IGNORE';
497 local $SIG{TSTP} = 'IGNORE';
498 local $SIG{PIPE} = 'IGNORE';
500 my $oldAutoCommit = $FS::UID::AutoCommit;
501 local $FS::UID::AutoCommit = 0;
504 my $error = $self->SUPER::replace($old);
507 $dbh->rollback if $oldAutoCommit;
511 if ( @param ) { # INVOICING_LIST_ARYREF
512 my $invoicing_list = shift @param;
513 $error = $self->check_invoicing_list( $invoicing_list );
515 $dbh->rollback if $oldAutoCommit;
518 $self->invoicing_list( $invoicing_list );
521 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
522 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
523 # card/check info has changed, want to retry realtime_card invoice events
524 #false laziness w/collect
525 foreach my $cust_bill_event (
527 #$_->part_bill_event->plan eq 'realtime-card'
528 $_->part_bill_event->eventcode =~
529 /^\$cust_bill\->realtime_(card|ach|lec)\(\);$/
530 && $_->status eq 'done'
533 map { $_->cust_bill_event }
534 grep { $_->cust_bill_event }
535 $self->open_cust_bill
538 my $error = $cust_bill_event->retry;
540 $dbh->rollback if $oldAutoCommit;
541 return "error scheduling invoice events for retry: $error";
548 #false laziness with sub insert
549 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
550 $error = $queue->insert($self->getfield('last'), $self->company);
552 $dbh->rollback if $oldAutoCommit;
553 return "queueing job (transaction rolled back): $error";
556 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
557 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
558 $error = $queue->insert($self->getfield('last'), $self->company);
560 $dbh->rollback if $oldAutoCommit;
561 return "queueing job (transaction rolled back): $error";
566 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
573 Checks all fields to make sure this is a valid customer record. If there is
574 an error, returns the error, otherwise returns false. Called by the insert
582 #warn "BEFORE: \n". $self->_dump;
585 $self->ut_numbern('custnum')
586 || $self->ut_number('agentnum')
587 || $self->ut_number('refnum')
588 || $self->ut_name('last')
589 || $self->ut_name('first')
590 || $self->ut_textn('company')
591 || $self->ut_text('address1')
592 || $self->ut_textn('address2')
593 || $self->ut_text('city')
594 || $self->ut_textn('county')
595 || $self->ut_textn('state')
596 || $self->ut_country('country')
597 || $self->ut_anything('comments')
598 || $self->ut_numbern('referral_custnum')
600 #barf. need message catalogs. i18n. etc.
601 $error .= "Please select a advertising source."
602 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
603 return $error if $error;
605 return "Unknown agent"
606 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
608 return "Unknown refnum"
609 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
611 return "Unknown referring custnum ". $self->referral_custnum
612 unless ! $self->referral_custnum
613 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
615 if ( $self->ss eq '' ) {
620 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
621 or return "Illegal social security number: ". $self->ss;
622 $self->ss("$1-$2-$3");
626 # bad idea to disable, causes billing to fail because of no tax rates later
627 # unless ( $import ) {
628 unless ( qsearch('cust_main_county', {
629 'country' => $self->country,
632 return "Unknown state/county/country: ".
633 $self->state. "/". $self->county. "/". $self->country
634 unless qsearch('cust_main_county',{
635 'state' => $self->state,
636 'county' => $self->county,
637 'country' => $self->country,
643 $self->ut_phonen('daytime', $self->country)
644 || $self->ut_phonen('night', $self->country)
645 || $self->ut_phonen('fax', $self->country)
646 || $self->ut_zip('zip', $self->country)
648 return $error if $error;
651 last first company address1 address2 city county state zip
652 country daytime night fax
655 if ( defined $self->dbdef_table->column('ship_last') ) {
656 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
658 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
662 $self->ut_name('ship_last')
663 || $self->ut_name('ship_first')
664 || $self->ut_textn('ship_company')
665 || $self->ut_text('ship_address1')
666 || $self->ut_textn('ship_address2')
667 || $self->ut_text('ship_city')
668 || $self->ut_textn('ship_county')
669 || $self->ut_textn('ship_state')
670 || $self->ut_country('ship_country')
672 return $error if $error;
674 #false laziness with above
675 unless ( qsearchs('cust_main_county', {
676 'country' => $self->ship_country,
679 return "Unknown ship_state/ship_county/ship_country: ".
680 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
681 unless qsearchs('cust_main_county',{
682 'state' => $self->ship_state,
683 'county' => $self->ship_county,
684 'country' => $self->ship_country,
690 $self->ut_phonen('ship_daytime', $self->ship_country)
691 || $self->ut_phonen('ship_night', $self->ship_country)
692 || $self->ut_phonen('ship_fax', $self->ship_country)
693 || $self->ut_zip('ship_zip', $self->ship_country)
695 return $error if $error;
697 } else { # ship_ info eq billing info, so don't store dup info in database
698 $self->setfield("ship_$_", '')
699 foreach qw( last first company address1 address2 city county state zip
700 country daytime night fax );
704 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
705 or return "Illegal payby: ". $self->payby;
708 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
710 my $payinfo = $self->payinfo;
712 $payinfo =~ /^(\d{13,16})$/
713 or return gettext('invalid_card'); # . ": ". $self->payinfo;
715 $self->payinfo($payinfo);
717 or return gettext('invalid_card'); # . ": ". $self->payinfo;
718 return gettext('unknown_card_type')
719 if cardtype($self->payinfo) eq "Unknown";
721 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
723 my $payinfo = $self->payinfo;
724 $payinfo =~ s/[^\d\@]//g;
725 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
727 $self->payinfo($payinfo);
729 } elsif ( $self->payby eq 'LECB' ) {
731 my $payinfo = $self->payinfo;
733 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
735 $self->payinfo($payinfo);
737 } elsif ( $self->payby eq 'BILL' ) {
739 $error = $self->ut_textn('payinfo');
740 return "Illegal P.O. number: ". $self->payinfo if $error;
742 } elsif ( $self->payby eq 'COMP' ) {
744 $error = $self->ut_textn('payinfo');
745 return "Illegal comp account issuer: ". $self->payinfo if $error;
747 } elsif ( $self->payby eq 'PREPAY' ) {
749 my $payinfo = $self->payinfo;
750 $payinfo =~ s/\W//g; #anything else would just confuse things
751 $self->payinfo($payinfo);
752 $error = $self->ut_alpha('payinfo');
753 return "Illegal prepayment identifier: ". $self->payinfo if $error;
754 return "Unknown prepayment identifier"
755 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
759 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
760 return "Expriation date required"
761 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
765 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
766 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
767 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) {
768 ( $m, $y ) = ( $3, "20$2" );
770 return "Illegal expiration date: ". $self->paydate;
772 $self->paydate("$y-$m-01");
773 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
774 return gettext('expired_card')
775 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
778 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
779 ( ! $conf->exists('require_cardname')
780 || $self->payby !~ /^(CARD|DCRD)$/ )
782 $self->payname( $self->first. " ". $self->getfield('last') );
784 $self->payname =~ /^([\w \,\.\-\']+)$/
785 or return gettext('illegal_name'). " payname: ". $self->payname;
789 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
792 $self->otaker(getotaker);
794 #warn "AFTER: \n". $self->_dump;
801 Returns all packages (see L<FS::cust_pkg>) for this customer.
807 if ( $self->{'_pkgnum'} ) {
808 values %{ $self->{'_pkgnum'}->cache };
810 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
814 =item ncancelled_pkgs
816 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
820 sub ncancelled_pkgs {
822 if ( $self->{'_pkgnum'} ) {
823 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
825 @{ [ # force list context
826 qsearch( 'cust_pkg', {
827 'custnum' => $self->custnum,
830 qsearch( 'cust_pkg', {
831 'custnum' => $self->custnum,
840 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
846 grep { $_->susp } $self->ncancelled_pkgs;
849 =item unflagged_suspended_pkgs
851 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
852 customer (thouse packages without the `manual_flag' set).
856 sub unflagged_suspended_pkgs {
858 return $self->suspended_pkgs
859 unless dbdef->table('cust_pkg')->column('manual_flag');
860 grep { ! $_->manual_flag } $self->suspended_pkgs;
863 =item unsuspended_pkgs
865 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
870 sub unsuspended_pkgs {
872 grep { ! $_->susp } $self->ncancelled_pkgs;
877 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
878 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
879 on success or a list of errors.
885 grep { $_->unsuspend } $self->suspended_pkgs;
890 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
891 Always returns a list: an empty list on success or a list of errors.
897 grep { $_->suspend } $self->unsuspended_pkgs;
902 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
903 Always returns a list: an empty list on success or a list of errors.
909 grep { $_->cancel } $self->ncancelled_pkgs;
914 Returns the agent (see L<FS::agent>) for this customer.
920 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
925 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
926 conjunction with the collect method.
928 Options are passed as name-value pairs.
930 The only currently available option is `time', which bills the customer as if
931 it were that time. It is specified as a UNIX timestamp; see
932 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
933 functions. For example:
937 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
939 If there is an error, returns the error, otherwise returns false.
944 my( $self, %options ) = @_;
945 my $time = $options{'time'} || time;
950 local $SIG{HUP} = 'IGNORE';
951 local $SIG{INT} = 'IGNORE';
952 local $SIG{QUIT} = 'IGNORE';
953 local $SIG{TERM} = 'IGNORE';
954 local $SIG{TSTP} = 'IGNORE';
955 local $SIG{PIPE} = 'IGNORE';
957 my $oldAutoCommit = $FS::UID::AutoCommit;
958 local $FS::UID::AutoCommit = 0;
961 # find the packages which are due for billing, find out how much they are
962 # & generate invoice database.
964 my( $total_setup, $total_recur ) = ( 0, 0 );
965 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
966 my @cust_bill_pkg = ();
968 #my $taxable_charged = 0;##
973 foreach my $cust_pkg (
974 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
977 #NO!! next if $cust_pkg->cancel;
978 next if $cust_pkg->getfield('cancel');
980 #? to avoid use of uninitialized value errors... ?
981 $cust_pkg->setfield('bill', '')
982 unless defined($cust_pkg->bill);
984 my $part_pkg = $cust_pkg->part_pkg;
986 #so we don't modify cust_pkg record unnecessarily
987 my $cust_pkg_mod_flag = 0;
988 my %hash = $cust_pkg->hash;
989 my $old_cust_pkg = new FS::cust_pkg \%hash;
995 unless ( $cust_pkg->setup ) {
996 my $setup_prog = $part_pkg->getfield('setup');
997 $setup_prog =~ /^(.*)$/ or do {
998 $dbh->rollback if $oldAutoCommit;
999 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
1003 $setup_prog = '0' if $setup_prog =~ /^\s*$/;
1005 #my $cpt = new Safe;
1006 ##$cpt->permit(); #what is necessary?
1007 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1008 #$setup = $cpt->reval($setup_prog);
1009 $setup = eval $setup_prog;
1010 unless ( defined($setup) ) {
1011 $dbh->rollback if $oldAutoCommit;
1012 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1013 "(expression $setup_prog): $@";
1015 $cust_pkg->setfield('setup',$time);
1016 $cust_pkg_mod_flag=1;
1022 if ( $part_pkg->getfield('freq') > 0 &&
1023 ! $cust_pkg->getfield('susp') &&
1024 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1026 my $recur_prog = $part_pkg->getfield('recur');
1027 $recur_prog =~ /^(.*)$/ or do {
1028 $dbh->rollback if $oldAutoCommit;
1029 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1033 $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1035 # shared with $recur_prog
1036 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1038 #my $cpt = new Safe;
1039 ##$cpt->permit(); #what is necessary?
1040 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1041 #$recur = $cpt->reval($recur_prog);
1042 $recur = eval $recur_prog;
1043 unless ( defined($recur) ) {
1044 $dbh->rollback if $oldAutoCommit;
1045 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1046 "(expression $recur_prog): $@";
1048 #change this bit to use Date::Manip? CAREFUL with timezones (see
1049 # mailing list archive)
1050 my ($sec,$min,$hour,$mday,$mon,$year) =
1051 (localtime($sdate) )[0,1,2,3,4,5];
1053 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1054 # only for figuring next bill date, nothing else, so, reset $sdate again
1056 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1057 $cust_pkg->last_bill($sdate)
1058 if $cust_pkg->dbdef_table->column('last_bill');
1060 $mon += $part_pkg->freq;
1061 until ( $mon < 12 ) { $mon -= 12; $year++; }
1062 $cust_pkg->setfield('bill',
1063 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1064 $cust_pkg_mod_flag = 1;
1067 warn "\$setup is undefined" unless defined($setup);
1068 warn "\$recur is undefined" unless defined($recur);
1069 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1071 my $taxable_charged = 0;
1072 if ( $cust_pkg_mod_flag ) {
1073 $error=$cust_pkg->replace($old_cust_pkg);
1074 if ( $error ) { #just in case
1075 $dbh->rollback if $oldAutoCommit;
1076 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1078 $setup = sprintf( "%.2f", $setup );
1079 $recur = sprintf( "%.2f", $recur );
1081 $dbh->rollback if $oldAutoCommit;
1082 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1085 $dbh->rollback if $oldAutoCommit;
1086 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1088 if ( $setup > 0 || $recur > 0 ) {
1089 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1090 'pkgnum' => $cust_pkg->pkgnum,
1094 'edate' => $cust_pkg->bill,
1095 'details' => \@details,
1097 push @cust_bill_pkg, $cust_bill_pkg;
1098 $total_setup += $setup;
1099 $total_recur += $recur;
1100 $taxable_charged += $setup
1101 unless $part_pkg->setuptax =~ /^Y$/i;
1102 $taxable_charged += $recur
1103 unless $part_pkg->recurtax =~ /^Y$/i;
1105 unless ( $self->tax =~ /Y/i
1106 || $self->payby eq 'COMP'
1107 || $taxable_charged == 0 ) {
1109 my $cust_main_county = qsearchs('cust_main_county',{
1110 'state' => $self->state,
1111 'county' => $self->county,
1112 'country' => $self->country,
1113 'taxclass' => $part_pkg->taxclass,
1115 $cust_main_county ||= qsearchs('cust_main_county',{
1116 'state' => $self->state,
1117 'county' => $self->county,
1118 'country' => $self->country,
1121 unless ( $cust_main_county ) {
1122 $dbh->rollback if $oldAutoCommit;
1124 "fatal: can't find tax rate for state/county/country/taxclass ".
1125 join('/', ( map $self->$_(), qw(state county country) ),
1126 $part_pkg->taxclass ). "\n";
1129 if ( $cust_main_county->exempt_amount ) {
1130 my ($mon,$year) = (localtime($sdate) )[4,5];
1132 my $freq = $part_pkg->freq || 1;
1133 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1134 foreach my $which_month ( 1 .. $freq ) {
1136 'custnum' => $self->custnum,
1137 'taxnum' => $cust_main_county->taxnum,
1138 'year' => 1900+$year,
1141 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1142 until ( $mon < 13 ) { $mon -= 12; $year++; }
1143 my $cust_tax_exempt =
1144 qsearchs('cust_tax_exempt', \%hash)
1145 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1146 my $remaining_exemption = sprintf("%.2f",
1147 $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
1148 if ( $remaining_exemption > 0 ) {
1149 my $addl = $remaining_exemption > $taxable_per_month
1150 ? $taxable_per_month
1151 : $remaining_exemption;
1152 $taxable_charged -= $addl;
1153 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1154 $cust_tax_exempt->hash,
1155 'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1157 $error = $new_cust_tax_exempt->exemptnum
1158 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1159 : $new_cust_tax_exempt->insert;
1161 $dbh->rollback if $oldAutoCommit;
1162 return "fatal: can't update cust_tax_exempt: $error";
1165 } # if $remaining_exemption > 0
1167 } #foreach $which_month
1169 } #if $cust_main_county->exempt_amount
1171 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1173 #$tax += $taxable_charged * $cust_main_county->tax / 100
1174 $tax{ $cust_main_county->taxname || 'Tax' } +=
1175 $taxable_charged * $cust_main_county->tax / 100
1177 } #unless $self->tax =~ /Y/i
1178 # || $self->payby eq 'COMP'
1179 # || $taxable_charged == 0
1181 } #if $setup > 0 || $recur > 0
1183 } #if $cust_pkg_mod_flag
1185 } #foreach my $cust_pkg
1187 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1188 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1190 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1191 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1195 # unless ( $self->tax =~ /Y/i
1196 # || $self->payby eq 'COMP'
1197 # || $taxable_charged == 0 ) {
1198 # my $cust_main_county = qsearchs('cust_main_county',{
1199 # 'state' => $self->state,
1200 # 'county' => $self->county,
1201 # 'country' => $self->country,
1202 # } ) or die "fatal: can't find tax rate for state/county/country ".
1203 # $self->state. "/". $self->county. "/". $self->country. "\n";
1204 # my $tax = sprintf( "%.2f",
1205 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1208 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1209 my $tax = sprintf("%.2f", $tax{$taxname} );
1210 $charged = sprintf( "%.2f", $charged+$tax );
1212 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1218 'itemdesc' => $taxname,
1220 push @cust_bill_pkg, $cust_bill_pkg;
1224 my $cust_bill = new FS::cust_bill ( {
1225 'custnum' => $self->custnum,
1227 'charged' => $charged,
1229 $error = $cust_bill->insert;
1231 $dbh->rollback if $oldAutoCommit;
1232 return "can't create invoice for customer #". $self->custnum. ": $error";
1235 my $invnum = $cust_bill->invnum;
1237 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1239 $cust_bill_pkg->invnum($invnum);
1240 $error = $cust_bill_pkg->insert;
1242 $dbh->rollback if $oldAutoCommit;
1243 return "can't create invoice line item for customer #". $self->custnum.
1248 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1252 =item collect OPTIONS
1254 (Attempt to) collect money for this customer's outstanding invoices (see
1255 L<FS::cust_bill>). Usually used after the bill method.
1257 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1258 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1259 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1261 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1262 and the invoice events web interface.
1264 If there is an error, returns the error, otherwise returns false.
1266 Options are passed as name-value pairs.
1268 Currently available options are:
1270 invoice_time - Use this time when deciding when to print invoices and
1271 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>
1272 for conversion functions.
1274 retry_card - Retry cards even when not scheduled by invoice events.
1276 batch_card - This option is deprecated. See the invoice events web interface
1277 to control whether cards are batched or run against a realtime gateway.
1279 report_badcard - This option is deprecated.
1281 force_print - This option is deprecated; see the invoice events web interface.
1286 my( $self, %options ) = @_;
1287 my $invoice_time = $options{'invoice_time'} || time;
1290 local $SIG{HUP} = 'IGNORE';
1291 local $SIG{INT} = 'IGNORE';
1292 local $SIG{QUIT} = 'IGNORE';
1293 local $SIG{TERM} = 'IGNORE';
1294 local $SIG{TSTP} = 'IGNORE';
1295 local $SIG{PIPE} = 'IGNORE';
1297 my $oldAutoCommit = $FS::UID::AutoCommit;
1298 local $FS::UID::AutoCommit = 0;
1301 my $balance = $self->balance;
1302 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1303 unless ( $balance > 0 ) { #redundant?????
1304 $dbh->rollback if $oldAutoCommit; #hmm
1308 if ( exists($options{'retry_card'}) && $options{'retry_card'} ) {
1309 #false laziness w/replace
1310 foreach my $cust_bill_event (
1312 #$_->part_bill_event->plan eq 'realtime-card'
1313 $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
1314 && $_->status eq 'done'
1317 map { $_->cust_bill_event }
1318 grep { $_->cust_bill_event }
1319 $self->open_cust_bill
1321 my $error = $cust_bill_event->retry;
1323 $dbh->rollback if $oldAutoCommit;
1324 return "error scheduling invoice events for retry: $error";
1330 foreach my $cust_bill ( $self->cust_bill ) {
1332 #this has to be before next's
1333 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1337 $balance = sprintf( "%.2f", $balance - $amount );
1339 next unless $cust_bill->owed > 0;
1341 # don't try to charge for the same invoice if it's already in a batch
1342 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1344 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1346 next unless $amount > 0;
1349 foreach my $part_bill_event (
1350 sort { $a->seconds <=> $b->seconds
1351 || $a->weight <=> $b->weight
1352 || $a->eventpart <=> $b->eventpart }
1353 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1354 && ! qsearchs( 'cust_bill_event', {
1355 'invnum' => $cust_bill->invnum,
1356 'eventpart' => $_->eventpart,
1360 qsearch('part_bill_event', { 'payby' => $self->payby,
1361 'disabled' => '', } )
1364 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1366 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1368 my $cust_main = $self; #for callback
1369 my $error = eval $part_bill_event->eventcode;
1372 my $statustext = '';
1376 } elsif ( $error ) {
1378 $statustext = $error;
1383 #add cust_bill_event
1384 my $cust_bill_event = new FS::cust_bill_event {
1385 'invnum' => $cust_bill->invnum,
1386 'eventpart' => $part_bill_event->eventpart,
1387 #'_date' => $invoice_time,
1389 'status' => $status,
1390 'statustext' => $statustext,
1392 $error = $cust_bill_event->insert;
1394 #$dbh->rollback if $oldAutoCommit;
1395 #return "error: $error";
1397 # gah, even with transactions.
1398 $dbh->commit if $oldAutoCommit; #well.
1399 my $e = 'WARNING: Event run but database not updated - '.
1400 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1401 ', eventpart '. $part_bill_event->eventpart.
1412 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1417 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1419 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1420 via a Business::OnlinePayment realtime gateway. See
1421 L<http://420.am/business-onlinepayment> for supported gateways.
1423 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1425 Available options are: I<description>, I<invnum>, I<quiet>
1427 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1428 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1429 if set, will override the value from the customer record.
1431 I<description> is a free-text field passed to the gateway. It defaults to
1432 "Internet services".
1434 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1435 specified invoice. If you don't specify an I<invnum> you might want to
1436 call the B<apply_payments> method.
1438 I<quiet> can be set true to surpress email decline notices.
1440 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1445 my( $self, $method, $amount, %options ) = @_;
1447 warn "$self $method $amount\n";
1448 warn " $_ => $options{$_}\n" foreach keys %options;
1451 $options{'description'} ||= 'Internet services';
1454 die "Real-time processing not enabled\n"
1455 unless $conf->exists('business-onlinepayment');
1456 eval "use Business::OnlinePayment";
1460 $self->set( $_ => $options{$_} )
1461 foreach grep { exists($options{$_}) }
1462 qw( payname address1 address2 city state zip payinfo paydate );
1465 my $bop_config = 'business-onlinepayment';
1466 $bop_config .= '-ach'
1467 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1468 my ( $processor, $login, $password, $action, @bop_options ) =
1469 $conf->config($bop_config);
1470 $action ||= 'normal authorization';
1471 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1475 my $address = $self->address1;
1476 $address .= ", ". $self->address2 if $self->address2;
1478 my($payname, $payfirst, $paylast);
1479 if ( $self->payname && $method ne 'ECHECK' ) {
1480 $payname = $self->payname;
1481 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1482 or return "Illegal payname $payname";
1483 ($payfirst, $paylast) = ($1, $2);
1485 $payfirst = $self->getfield('first');
1486 $paylast = $self->getfield('last');
1487 $payname = "$payfirst $paylast";
1490 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1491 if ( $conf->exists('emailinvoiceauto')
1492 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1493 push @invoicing_list, $self->all_emails;
1495 my $email = $invoicing_list[0];
1498 if ( $method eq 'CC' ) {
1499 $content{card_number} = $self->payinfo;
1500 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1501 $content{expiration} = "$2/$1";
1502 } elsif ( $method eq 'ECHECK' ) {
1503 my($account_number,$routing_code) = $self->payinfo;
1504 ( $content{account_number}, $content{routing_code} ) =
1505 split('@', $self->payinfo);
1506 $content{bank_name} = $self->payname;
1507 } elsif ( $method eq 'LEC' ) {
1508 $content{phone} = $self->payinfo;
1513 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1516 new Business::OnlinePayment( $processor, @bop_options );
1517 $transaction->content(
1520 'password' => $password,
1521 'action' => $action1,
1522 'description' => $options{'description'},
1523 'amount' => $amount,
1524 'invoice_number' => $options{'invnum'},
1525 'customer_id' => $self->custnum,
1526 'last_name' => $paylast,
1527 'first_name' => $payfirst,
1529 'address' => $address,
1530 'city' => $self->city,
1531 'state' => $self->state,
1532 'zip' => $self->zip,
1533 'country' => $self->country,
1534 'referer' => 'http://cleanwhisker.420.am/',
1536 'phone' => $self->daytime || $self->night,
1539 $transaction->submit();
1541 if ( $transaction->is_success() && $action2 ) {
1542 my $auth = $transaction->authorization;
1543 my $ordernum = $transaction->can('order_number')
1544 ? $transaction->order_number
1548 new Business::OnlinePayment( $processor, @bop_options );
1555 password => $password,
1556 order_number => $ordernum,
1558 authorization => $auth,
1559 description => $options{'description'},
1562 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1563 transaction_sequence_num local_transaction_date
1564 local_transaction_time AVS_result_code )) {
1565 $capture{$field} = $transaction->$field() if $transaction->can($field);
1568 $capture->content( %capture );
1572 unless ( $capture->is_success ) {
1573 my $e = "Authorization sucessful but capture failed, custnum #".
1574 $self->custnum. ': '. $capture->result_code.
1575 ": ". $capture->error_message;
1583 if ( $transaction->is_success() ) {
1585 my %method2payby = (
1591 my $cust_pay = new FS::cust_pay ( {
1592 'custnum' => $self->custnum,
1593 'invnum' => $options{'invnum'},
1596 'payby' => $method2payby{$method},
1597 'payinfo' => $self->payinfo,
1598 'paybatch' => "$processor:". $transaction->authorization,
1600 my $error = $cust_pay->insert;
1602 # gah, even with transactions.
1603 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1604 'error applying payment, invnum #' . $self->invnum.
1605 " ($processor): $error";
1614 my $perror = "$processor error: ". $transaction->error_message;
1616 if ( !$options{'quiet'} && $conf->exists('emaildecline')
1617 && grep { $_ ne 'POST' } $self->invoicing_list
1619 my @templ = $conf->config('declinetemplate');
1620 my $template = new Text::Template (
1622 SOURCE => [ map "$_\n", @templ ],
1623 ) or return "($perror) can't create template: $Text::Template::ERROR";
1624 $template->compile()
1625 or return "($perror) can't compile template: $Text::Template::ERROR";
1627 my $templ_hash = { error => $transaction->error_message };
1629 my $error = send_email(
1630 'from' => $conf->config('invoice_from'),
1631 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1632 'subject' => 'Your payment could not be processed',
1633 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1636 $perror .= " (also received error sending decline notification: $error)"
1648 Returns the total owed for this customer on all invoices
1649 (see L<FS::cust_bill/owed>).
1655 $self->total_owed_date(2145859200); #12/31/2037
1658 =item total_owed_date TIME
1660 Returns the total owed for this customer on all invoices with date earlier than
1661 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1662 see L<Time::Local> and L<Date::Parse> for conversion functions.
1666 sub total_owed_date {
1670 foreach my $cust_bill (
1671 grep { $_->_date <= $time }
1672 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1674 $total_bill += $cust_bill->owed;
1676 sprintf( "%.2f", $total_bill );
1681 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1682 to outstanding invoice balances in chronological order and returns the value
1683 of any remaining unapplied credits available for refund
1684 (see L<FS::cust_refund>).
1691 return 0 unless $self->total_credited;
1693 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1694 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1696 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1697 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1701 foreach my $cust_bill ( @invoices ) {
1704 if ( !defined($credit) || $credit->credited == 0) {
1705 $credit = pop @credits or last;
1708 if ($cust_bill->owed >= $credit->credited) {
1709 $amount=$credit->credited;
1711 $amount=$cust_bill->owed;
1714 my $cust_credit_bill = new FS::cust_credit_bill ( {
1715 'crednum' => $credit->crednum,
1716 'invnum' => $cust_bill->invnum,
1717 'amount' => $amount,
1719 my $error = $cust_credit_bill->insert;
1720 die $error if $error;
1722 redo if ($cust_bill->owed > 0);
1726 return $self->total_credited;
1729 =item apply_payments
1731 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1732 to outstanding invoice balances in chronological order.
1734 #and returns the value of any remaining unapplied payments.
1738 sub apply_payments {
1743 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1744 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1746 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1747 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1751 foreach my $cust_bill ( @invoices ) {
1754 if ( !defined($payment) || $payment->unapplied == 0 ) {
1755 $payment = pop @payments or last;
1758 if ( $cust_bill->owed >= $payment->unapplied ) {
1759 $amount = $payment->unapplied;
1761 $amount = $cust_bill->owed;
1764 my $cust_bill_pay = new FS::cust_bill_pay ( {
1765 'paynum' => $payment->paynum,
1766 'invnum' => $cust_bill->invnum,
1767 'amount' => $amount,
1769 my $error = $cust_bill_pay->insert;
1770 die $error if $error;
1772 redo if ( $cust_bill->owed > 0);
1776 return $self->total_unapplied_payments;
1779 =item total_credited
1781 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1782 customer. See L<FS::cust_credit/credited>.
1786 sub total_credited {
1788 my $total_credit = 0;
1789 foreach my $cust_credit ( qsearch('cust_credit', {
1790 'custnum' => $self->custnum,
1792 $total_credit += $cust_credit->credited;
1794 sprintf( "%.2f", $total_credit );
1797 =item total_unapplied_payments
1799 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1800 See L<FS::cust_pay/unapplied>.
1804 sub total_unapplied_payments {
1806 my $total_unapplied = 0;
1807 foreach my $cust_pay ( qsearch('cust_pay', {
1808 'custnum' => $self->custnum,
1810 $total_unapplied += $cust_pay->unapplied;
1812 sprintf( "%.2f", $total_unapplied );
1817 Returns the balance for this customer (total_owed minus total_credited
1818 minus total_unapplied_payments).
1825 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1829 =item balance_date TIME
1831 Returns the balance for this customer, only considering invoices with date
1832 earlier than TIME (total_owed_date minus total_credited minus
1833 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1834 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1843 $self->total_owed_date($time)
1844 - $self->total_credited
1845 - $self->total_unapplied_payments
1849 =item invoicing_list [ ARRAYREF ]
1851 If an arguement is given, sets these email addresses as invoice recipients
1852 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1853 (except as warnings), so use check_invoicing_list first.
1855 Returns a list of email addresses (with svcnum entries expanded).
1857 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1858 check it without disturbing anything by passing nothing.
1860 This interface may change in the future.
1864 sub invoicing_list {
1865 my( $self, $arrayref ) = @_;
1867 my @cust_main_invoice;
1868 if ( $self->custnum ) {
1869 @cust_main_invoice =
1870 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1872 @cust_main_invoice = ();
1874 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1875 #warn $cust_main_invoice->destnum;
1876 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1877 #warn $cust_main_invoice->destnum;
1878 my $error = $cust_main_invoice->delete;
1879 warn $error if $error;
1882 if ( $self->custnum ) {
1883 @cust_main_invoice =
1884 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1886 @cust_main_invoice = ();
1888 my %seen = map { $_->address => 1 } @cust_main_invoice;
1889 foreach my $address ( @{$arrayref} ) {
1890 next if exists $seen{$address} && $seen{$address};
1891 $seen{$address} = 1;
1892 my $cust_main_invoice = new FS::cust_main_invoice ( {
1893 'custnum' => $self->custnum,
1896 my $error = $cust_main_invoice->insert;
1897 warn $error if $error;
1900 if ( $self->custnum ) {
1902 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1908 =item check_invoicing_list ARRAYREF
1910 Checks these arguements as valid input for the invoicing_list method. If there
1911 is an error, returns the error, otherwise returns false.
1915 sub check_invoicing_list {
1916 my( $self, $arrayref ) = @_;
1917 foreach my $address ( @{$arrayref} ) {
1918 my $cust_main_invoice = new FS::cust_main_invoice ( {
1919 'custnum' => $self->custnum,
1922 my $error = $self->custnum
1923 ? $cust_main_invoice->check
1924 : $cust_main_invoice->checkdest
1926 return $error if $error;
1931 =item set_default_invoicing_list
1933 Sets the invoicing list to all accounts associated with this customer,
1934 overwriting any previous invoicing list.
1938 sub set_default_invoicing_list {
1940 $self->invoicing_list($self->all_emails);
1945 Returns the email addresses of all accounts provisioned for this customer.
1952 foreach my $cust_pkg ( $self->all_pkgs ) {
1953 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1955 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1956 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1958 $list{$_}=1 foreach map { $_->email } @svc_acct;
1963 =item invoicing_list_addpost
1965 Adds postal invoicing to this customer. If this customer is already configured
1966 to receive postal invoices, does nothing.
1970 sub invoicing_list_addpost {
1972 return if grep { $_ eq 'POST' } $self->invoicing_list;
1973 my @invoicing_list = $self->invoicing_list;
1974 push @invoicing_list, 'POST';
1975 $self->invoicing_list(\@invoicing_list);
1978 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1980 Returns an array of customers referred by this customer (referral_custnum set
1981 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1982 customers referred by customers referred by this customer and so on, inclusive.
1983 The default behavior is DEPTH 1 (no recursion).
1987 sub referral_cust_main {
1989 my $depth = @_ ? shift : 1;
1990 my $exclude = @_ ? shift : {};
1993 map { $exclude->{$_->custnum}++; $_; }
1994 grep { ! $exclude->{ $_->custnum } }
1995 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1999 map { $_->referral_cust_main($depth-1, $exclude) }
2006 =item referral_cust_main_ncancelled
2008 Same as referral_cust_main, except only returns customers with uncancelled
2013 sub referral_cust_main_ncancelled {
2015 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2018 =item referral_cust_pkg [ DEPTH ]
2020 Like referral_cust_main, except returns a flat list of all unsuspended (and
2021 uncancelled) packages for each customer. The number of items in this list may
2022 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2026 sub referral_cust_pkg {
2028 my $depth = @_ ? shift : 1;
2030 map { $_->unsuspended_pkgs }
2031 grep { $_->unsuspended_pkgs }
2032 $self->referral_cust_main($depth);
2035 =item credit AMOUNT, REASON
2037 Applies a credit to this customer. If there is an error, returns the error,
2038 otherwise returns false.
2043 my( $self, $amount, $reason ) = @_;
2044 my $cust_credit = new FS::cust_credit {
2045 'custnum' => $self->custnum,
2046 'amount' => $amount,
2047 'reason' => $reason,
2049 $cust_credit->insert;
2052 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2054 Creates a one-time charge for this customer. If there is an error, returns
2055 the error, otherwise returns false.
2060 my ( $self, $amount ) = ( shift, shift );
2061 my $pkg = @_ ? shift : 'One-time charge';
2062 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2063 my $taxclass = @_ ? shift : '';
2065 local $SIG{HUP} = 'IGNORE';
2066 local $SIG{INT} = 'IGNORE';
2067 local $SIG{QUIT} = 'IGNORE';
2068 local $SIG{TERM} = 'IGNORE';
2069 local $SIG{TSTP} = 'IGNORE';
2070 local $SIG{PIPE} = 'IGNORE';
2072 my $oldAutoCommit = $FS::UID::AutoCommit;
2073 local $FS::UID::AutoCommit = 0;
2076 my $part_pkg = new FS::part_pkg ( {
2078 'comment' => $comment,
2083 'taxclass' => $taxclass,
2086 my $error = $part_pkg->insert;
2088 $dbh->rollback if $oldAutoCommit;
2092 my $pkgpart = $part_pkg->pkgpart;
2093 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2094 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2095 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2096 $error = $type_pkgs->insert;
2098 $dbh->rollback if $oldAutoCommit;
2103 my $cust_pkg = new FS::cust_pkg ( {
2104 'custnum' => $self->custnum,
2105 'pkgpart' => $pkgpart,
2108 $error = $cust_pkg->insert;
2110 $dbh->rollback if $oldAutoCommit;
2114 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2121 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2127 sort { $a->_date <=> $b->_date }
2128 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2131 =item open_cust_bill
2133 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2138 sub open_cust_bill {
2140 grep { $_->owed > 0 } $self->cust_bill;
2149 =item check_and_rebuild_fuzzyfiles
2153 sub check_and_rebuild_fuzzyfiles {
2154 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2155 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2156 or &rebuild_fuzzyfiles;
2159 =item rebuild_fuzzyfiles
2163 sub rebuild_fuzzyfiles {
2165 use Fcntl qw(:flock);
2167 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2171 open(LASTLOCK,">>$dir/cust_main.last")
2172 or die "can't open $dir/cust_main.last: $!";
2173 flock(LASTLOCK,LOCK_EX)
2174 or die "can't lock $dir/cust_main.last: $!";
2176 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2178 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2179 if defined dbdef->table('cust_main')->column('ship_last');
2181 open (LASTCACHE,">$dir/cust_main.last.tmp")
2182 or die "can't open $dir/cust_main.last.tmp: $!";
2183 print LASTCACHE join("\n", @all_last), "\n";
2184 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2186 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
2191 open(COMPANYLOCK,">>$dir/cust_main.company")
2192 or die "can't open $dir/cust_main.company: $!";
2193 flock(COMPANYLOCK,LOCK_EX)
2194 or die "can't lock $dir/cust_main.company: $!";
2196 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
2198 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
2199 if defined dbdef->table('cust_main')->column('ship_last');
2201 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
2202 or die "can't open $dir/cust_main.company.tmp: $!";
2203 print COMPANYCACHE join("\n", @all_company), "\n";
2204 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
2206 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
2216 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2217 open(LASTCACHE,"<$dir/cust_main.last")
2218 or die "can't open $dir/cust_main.last: $!";
2219 my @array = map { chomp; $_; } <LASTCACHE>;
2229 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2230 open(COMPANYCACHE,"<$dir/cust_main.company")
2231 or die "can't open $dir/cust_main.last: $!";
2232 my @array = map { chomp; $_; } <COMPANYCACHE>;
2237 =item append_fuzzyfiles LASTNAME COMPANY
2241 sub append_fuzzyfiles {
2242 my( $last, $company ) = @_;
2244 &check_and_rebuild_fuzzyfiles;
2246 use Fcntl qw(:flock);
2248 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2252 open(LAST,">>$dir/cust_main.last")
2253 or die "can't open $dir/cust_main.last: $!";
2255 or die "can't lock $dir/cust_main.last: $!";
2257 print LAST "$last\n";
2260 or die "can't unlock $dir/cust_main.last: $!";
2266 open(COMPANY,">>$dir/cust_main.company")
2267 or die "can't open $dir/cust_main.company: $!";
2268 flock(COMPANY,LOCK_EX)
2269 or die "can't lock $dir/cust_main.company: $!";
2271 print COMPANY "$company\n";
2273 flock(COMPANY,LOCK_UN)
2274 or die "can't unlock $dir/cust_main.company: $!";
2288 #warn join('-',keys %$param);
2289 my $fh = $param->{filehandle};
2290 my $agentnum = $param->{agentnum};
2291 my $refnum = $param->{refnum};
2292 my $pkgpart = $param->{pkgpart};
2293 my @fields = @{$param->{fields}};
2295 eval "use Date::Parse;";
2297 eval "use Text::CSV_XS;";
2300 my $csv = new Text::CSV_XS;
2307 local $SIG{HUP} = 'IGNORE';
2308 local $SIG{INT} = 'IGNORE';
2309 local $SIG{QUIT} = 'IGNORE';
2310 local $SIG{TERM} = 'IGNORE';
2311 local $SIG{TSTP} = 'IGNORE';
2312 local $SIG{PIPE} = 'IGNORE';
2314 my $oldAutoCommit = $FS::UID::AutoCommit;
2315 local $FS::UID::AutoCommit = 0;
2318 #while ( $columns = $csv->getline($fh) ) {
2320 while ( defined($line=<$fh>) ) {
2322 $csv->parse($line) or do {
2323 $dbh->rollback if $oldAutoCommit;
2324 return "can't parse: ". $csv->error_input();
2327 my @columns = $csv->fields();
2328 #warn join('-',@columns);
2331 agentnum => $agentnum,
2333 country => 'US', #default
2334 payby => 'BILL', #default
2335 paydate => '12/2037', #default
2337 my $billtime = time;
2338 my %cust_pkg = ( pkgpart => $pkgpart );
2339 foreach my $field ( @fields ) {
2340 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2341 #$cust_pkg{$1} = str2time( shift @$columns );
2342 if ( $1 eq 'setup' ) {
2343 $billtime = str2time(shift @columns);
2345 $cust_pkg{$1} = str2time( shift @columns );
2348 #$cust_main{$field} = shift @$columns;
2349 $cust_main{$field} = shift @columns;
2353 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2354 my $cust_main = new FS::cust_main ( \%cust_main );
2356 tie my %hash, 'Tie::RefHash'; #this part is important
2357 $hash{$cust_pkg} = [] if $pkgpart;
2358 my $error = $cust_main->insert( \%hash );
2361 $dbh->rollback if $oldAutoCommit;
2362 return "can't insert customer for $line: $error";
2365 #false laziness w/bill.cgi
2366 $error = $cust_main->bill( 'time' => $billtime );
2368 $dbh->rollback if $oldAutoCommit;
2369 return "can't bill customer for $line: $error";
2372 $cust_main->apply_payments;
2373 $cust_main->apply_credits;
2375 $error = $cust_main->collect();
2377 $dbh->rollback if $oldAutoCommit;
2378 return "can't collect customer for $line: $error";
2384 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2386 return "Empty file!" unless $imported;
2398 #warn join('-',keys %$param);
2399 my $fh = $param->{filehandle};
2400 my @fields = @{$param->{fields}};
2402 eval "use Date::Parse;";
2404 eval "use Text::CSV_XS;";
2407 my $csv = new Text::CSV_XS;
2414 local $SIG{HUP} = 'IGNORE';
2415 local $SIG{INT} = 'IGNORE';
2416 local $SIG{QUIT} = 'IGNORE';
2417 local $SIG{TERM} = 'IGNORE';
2418 local $SIG{TSTP} = 'IGNORE';
2419 local $SIG{PIPE} = 'IGNORE';
2421 my $oldAutoCommit = $FS::UID::AutoCommit;
2422 local $FS::UID::AutoCommit = 0;
2425 #while ( $columns = $csv->getline($fh) ) {
2427 while ( defined($line=<$fh>) ) {
2429 $csv->parse($line) or do {
2430 $dbh->rollback if $oldAutoCommit;
2431 return "can't parse: ". $csv->error_input();
2434 my @columns = $csv->fields();
2435 #warn join('-',@columns);
2438 foreach my $field ( @fields ) {
2439 $row{$field} = shift @columns;
2442 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2443 unless ( $cust_main ) {
2444 $dbh->rollback if $oldAutoCommit;
2445 return "unknown custnum $row{'custnum'}";
2448 if ( $row{'amount'} > 0 ) {
2449 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2451 $dbh->rollback if $oldAutoCommit;
2455 } elsif ( $row{'amount'} < 0 ) {
2456 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2459 $dbh->rollback if $oldAutoCommit;
2469 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2471 return "Empty file!" unless $imported;
2483 The delete method should possibly take an FS::cust_main object reference
2484 instead of a scalar customer number.
2486 Bill and collect options should probably be passed as references instead of a
2489 There should probably be a configuration file with a list of allowed credit
2492 No multiple currency support (probably a larger project than just this module).
2496 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2497 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2498 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.