4 use vars qw( @ISA $conf $Debug $import );
10 use Business::CreditCard;
11 use FS::UID qw( getotaker dbh );
12 use FS::Record qw( qsearchs qsearch dbdef );
15 use FS::cust_bill_pkg;
18 use FS::part_referral;
19 use FS::cust_main_county;
21 use FS::cust_main_invoice;
22 use FS::cust_credit_bill;
23 use FS::cust_bill_pay;
24 use FS::prepay_credit;
27 use FS::part_bill_event;
28 use FS::cust_bill_event;
29 use FS::cust_tax_exempt;
31 use FS::Msgcat qw(gettext);
33 @ISA = qw( FS::Record );
40 #ask FS::UID to run this stuff for us later
41 #$FS::UID::callback{'FS::cust_main'} = sub {
42 install_callback FS::UID sub {
44 #yes, need it for stuff below (prolly should be cached)
49 my ( $hashref, $cache ) = @_;
50 if ( exists $hashref->{'pkgnum'} ) {
51 # #@{ $self->{'_pkgnum'} } = ();
52 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
53 $self->{'_pkgnum'} = $subcache;
54 #push @{ $self->{'_pkgnum'} },
55 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
61 FS::cust_main - Object methods for cust_main records
67 $record = new FS::cust_main \%hash;
68 $record = new FS::cust_main { 'column' => 'value' };
70 $error = $record->insert;
72 $error = $new_record->replace($old_record);
74 $error = $record->delete;
76 $error = $record->check;
78 @cust_pkg = $record->all_pkgs;
80 @cust_pkg = $record->ncancelled_pkgs;
82 @cust_pkg = $record->suspended_pkgs;
84 $error = $record->bill;
85 $error = $record->bill %options;
86 $error = $record->bill 'time' => $time;
88 $error = $record->collect;
89 $error = $record->collect %options;
90 $error = $record->collect 'invoice_time' => $time,
91 'batch_card' => 'yes',
92 'report_badcard' => 'yes',
97 An FS::cust_main object represents a customer. FS::cust_main inherits from
98 FS::Record. The following fields are currently supported:
102 =item custnum - primary key (assigned automatically for new customers)
104 =item agentnum - agent (see L<FS::agent>)
106 =item refnum - Advertising source (see L<FS::part_referral>)
112 =item ss - social security number (optional)
114 =item company - (optional)
118 =item address2 - (optional)
122 =item county - (optional, see L<FS::cust_main_county>)
124 =item state - (see L<FS::cust_main_county>)
128 =item country - (see L<FS::cust_main_county>)
130 =item daytime - phone (optional)
132 =item night - phone (optional)
134 =item fax - phone (optional)
136 =item ship_first - name
138 =item ship_last - name
140 =item ship_company - (optional)
144 =item ship_address2 - (optional)
148 =item ship_county - (optional, see L<FS::cust_main_county>)
150 =item ship_state - (see L<FS::cust_main_county>)
154 =item ship_country - (see L<FS::cust_main_county>)
156 =item ship_daytime - phone (optional)
158 =item ship_night - phone (optional)
160 =item ship_fax - phone (optional)
162 =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>)
164 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
166 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
168 =item payname - name on card or billing name
170 =item tax - tax exempt, empty or `Y'
172 =item otaker - order taker (assigned automatically, see L<FS::UID>)
174 =item comments - comments (optional)
176 =item referral_custnum - referring customer number
186 Creates a new customer. To add the customer to the database, see L<"insert">.
188 Note that this stores the hash reference, not a distinct copy of the hash it
189 points to. You can ask the object for a copy with the I<hash> method.
193 sub table { 'cust_main'; }
195 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
197 Adds this customer to the database. If there is an error, returns the error,
198 otherwise returns false.
200 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
201 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
202 are inserted atomicly, or the transaction is rolled back. Passing an empty
203 hash reference is equivalent to not supplying this parameter. There should be
204 a better explanation of this, but until then, here's an example:
207 tie %hash, 'Tie::RefHash'; #this part is important
209 $cust_pkg => [ $svc_acct ],
212 $cust_main->insert( \%hash );
214 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
215 be set as the invoicing list (see L<"invoicing_list">). Errors return as
216 expected and rollback the entire transaction; it is not necessary to call
217 check_invoicing_list first. The invoicing_list is set after the records in the
218 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
219 invoicing_list destination to the newly-created svc_acct. Here's an example:
221 $cust_main->insert( {}, [ $email, 'POST' ] );
227 my $cust_pkgs = @_ ? shift : {};
228 my $invoicing_list = @_ ? shift : '';
230 local $SIG{HUP} = 'IGNORE';
231 local $SIG{INT} = 'IGNORE';
232 local $SIG{QUIT} = 'IGNORE';
233 local $SIG{TERM} = 'IGNORE';
234 local $SIG{TSTP} = 'IGNORE';
235 local $SIG{PIPE} = 'IGNORE';
237 my $oldAutoCommit = $FS::UID::AutoCommit;
238 local $FS::UID::AutoCommit = 0;
243 if ( $self->payby eq 'PREPAY' ) {
244 $self->payby('BILL');
245 my $prepay_credit = qsearchs(
247 { 'identifier' => $self->payinfo },
251 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
252 unless $prepay_credit;
253 $amount = $prepay_credit->amount;
254 $seconds = $prepay_credit->seconds;
255 my $error = $prepay_credit->delete;
257 $dbh->rollback if $oldAutoCommit;
258 return "removing prepay_credit (transaction rolled back): $error";
262 my $error = $self->SUPER::insert;
264 $dbh->rollback if $oldAutoCommit;
265 #return "inserting cust_main record (transaction rolled back): $error";
270 if ( $invoicing_list ) {
271 $error = $self->check_invoicing_list( $invoicing_list );
273 $dbh->rollback if $oldAutoCommit;
274 return "checking invoicing_list (transaction rolled back): $error";
276 $self->invoicing_list( $invoicing_list );
280 $error = $self->order_pkgs($cust_pkgs, \$seconds);
282 $dbh->rollback if $oldAutoCommit;
287 $dbh->rollback if $oldAutoCommit;
288 return "No svc_acct record to apply pre-paid time";
292 my $cust_credit = new FS::cust_credit {
293 'custnum' => $self->custnum,
296 $error = $cust_credit->insert;
298 $dbh->rollback if $oldAutoCommit;
299 return "inserting credit (transaction rolled back): $error";
303 #false laziness with sub replace
304 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
305 $error = $queue->insert($self->getfield('last'), $self->company);
307 $dbh->rollback if $oldAutoCommit;
308 return "queueing job (transaction rolled back): $error";
311 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
312 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
313 $error = $queue->insert($self->getfield('last'), $self->company);
315 $dbh->rollback if $oldAutoCommit;
316 return "queueing job (transaction rolled back): $error";
321 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
328 document me. like ->insert(%cust_pkg) on an existing record
334 my $cust_pkgs = shift;
337 local $SIG{HUP} = 'IGNORE';
338 local $SIG{INT} = 'IGNORE';
339 local $SIG{QUIT} = 'IGNORE';
340 local $SIG{TERM} = 'IGNORE';
341 local $SIG{TSTP} = 'IGNORE';
342 local $SIG{PIPE} = 'IGNORE';
344 my $oldAutoCommit = $FS::UID::AutoCommit;
345 local $FS::UID::AutoCommit = 0;
348 foreach my $cust_pkg ( keys %$cust_pkgs ) {
349 $cust_pkg->custnum( $self->custnum );
350 my $error = $cust_pkg->insert;
352 $dbh->rollback if $oldAutoCommit;
353 return "inserting cust_pkg (transaction rolled back): $error";
355 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
356 $svc_something->pkgnum( $cust_pkg->pkgnum );
357 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
358 $svc_something->seconds( $svc_something->seconds + $$seconds );
361 $error = $svc_something->insert;
363 $dbh->rollback if $oldAutoCommit;
364 #return "inserting svc_ (transaction rolled back): $error";
370 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
374 =item delete NEW_CUSTNUM
376 This deletes the customer. If there is an error, returns the error, otherwise
379 This will completely remove all traces of the customer record. This is not
380 what you want when a customer cancels service; for that, cancel all of the
381 customer's packages (see L<FS::cust_pkg/cancel>).
383 If the customer has any uncancelled packages, you need to pass a new (valid)
384 customer number for those packages to be transferred to. Cancelled packages
385 will be deleted. Did I mention that this is NOT what you want when a customer
386 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
388 You can't delete a customer with invoices (see L<FS::cust_bill>),
389 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
390 refunds (see L<FS::cust_refund>).
397 local $SIG{HUP} = 'IGNORE';
398 local $SIG{INT} = 'IGNORE';
399 local $SIG{QUIT} = 'IGNORE';
400 local $SIG{TERM} = 'IGNORE';
401 local $SIG{TSTP} = 'IGNORE';
402 local $SIG{PIPE} = 'IGNORE';
404 my $oldAutoCommit = $FS::UID::AutoCommit;
405 local $FS::UID::AutoCommit = 0;
408 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
409 $dbh->rollback if $oldAutoCommit;
410 return "Can't delete a customer with invoices";
412 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
413 $dbh->rollback if $oldAutoCommit;
414 return "Can't delete a customer with credits";
416 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
417 $dbh->rollback if $oldAutoCommit;
418 return "Can't delete a customer with payments";
420 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
421 $dbh->rollback if $oldAutoCommit;
422 return "Can't delete a customer with refunds";
425 my @cust_pkg = $self->ncancelled_pkgs;
427 my $new_custnum = shift;
428 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
429 $dbh->rollback if $oldAutoCommit;
430 return "Invalid new customer number: $new_custnum";
432 foreach my $cust_pkg ( @cust_pkg ) {
433 my %hash = $cust_pkg->hash;
434 $hash{'custnum'} = $new_custnum;
435 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
436 my $error = $new_cust_pkg->replace($cust_pkg);
438 $dbh->rollback if $oldAutoCommit;
443 my @cancelled_cust_pkg = $self->all_pkgs;
444 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
445 my $error = $cust_pkg->delete;
447 $dbh->rollback if $oldAutoCommit;
452 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
453 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
455 my $error = $cust_main_invoice->delete;
457 $dbh->rollback if $oldAutoCommit;
462 my $error = $self->SUPER::delete;
464 $dbh->rollback if $oldAutoCommit;
468 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
473 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
475 Replaces the OLD_RECORD with this one in the database. If there is an error,
476 returns the error, otherwise returns false.
478 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
479 be set as the invoicing list (see L<"invoicing_list">). Errors return as
480 expected and rollback the entire transaction; it is not necessary to call
481 check_invoicing_list first. Here's an example:
483 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
492 local $SIG{HUP} = 'IGNORE';
493 local $SIG{INT} = 'IGNORE';
494 local $SIG{QUIT} = 'IGNORE';
495 local $SIG{TERM} = 'IGNORE';
496 local $SIG{TSTP} = 'IGNORE';
497 local $SIG{PIPE} = 'IGNORE';
499 my $oldAutoCommit = $FS::UID::AutoCommit;
500 local $FS::UID::AutoCommit = 0;
503 my $error = $self->SUPER::replace($old);
506 $dbh->rollback if $oldAutoCommit;
510 if ( @param ) { # INVOICING_LIST_ARYREF
511 my $invoicing_list = shift @param;
512 $error = $self->check_invoicing_list( $invoicing_list );
514 $dbh->rollback if $oldAutoCommit;
517 $self->invoicing_list( $invoicing_list );
520 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
521 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
522 # card/check info has changed, want to retry realtime_card invoice events
523 #false laziness w/collect
524 foreach my $cust_bill_event (
526 #$_->part_bill_event->plan eq 'realtime-card'
527 $_->part_bill_event->eventcode =~
528 /^\$cust_bill\->realtime_(card|ach|lec)\(\);$/
529 && $_->status eq 'done'
532 map { $_->cust_bill_event }
533 grep { $_->cust_bill_event }
534 $self->open_cust_bill
537 my $error = $cust_bill_event->retry;
539 $dbh->rollback if $oldAutoCommit;
540 return "error scheduling invoice events for retry: $error";
547 #false laziness with sub insert
548 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
549 $error = $queue->insert($self->getfield('last'), $self->company);
551 $dbh->rollback if $oldAutoCommit;
552 return "queueing job (transaction rolled back): $error";
555 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
556 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
557 $error = $queue->insert($self->getfield('last'), $self->company);
559 $dbh->rollback if $oldAutoCommit;
560 return "queueing job (transaction rolled back): $error";
565 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
572 Checks all fields to make sure this is a valid customer record. If there is
573 an error, returns the error, otherwise returns false. Called by the insert
581 #warn "BEFORE: \n". $self->_dump;
584 $self->ut_numbern('custnum')
585 || $self->ut_number('agentnum')
586 || $self->ut_number('refnum')
587 || $self->ut_name('last')
588 || $self->ut_name('first')
589 || $self->ut_textn('company')
590 || $self->ut_text('address1')
591 || $self->ut_textn('address2')
592 || $self->ut_text('city')
593 || $self->ut_textn('county')
594 || $self->ut_textn('state')
595 || $self->ut_country('country')
596 || $self->ut_anything('comments')
597 || $self->ut_numbern('referral_custnum')
599 #barf. need message catalogs. i18n. etc.
600 $error .= "Please select a advertising source."
601 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
602 return $error if $error;
604 return "Unknown agent"
605 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
607 return "Unknown refnum"
608 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
610 return "Unknown referring custnum ". $self->referral_custnum
611 unless ! $self->referral_custnum
612 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
614 if ( $self->ss eq '' ) {
619 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
620 or return "Illegal social security number: ". $self->ss;
621 $self->ss("$1-$2-$3");
625 # bad idea to disable, causes billing to fail because of no tax rates later
626 # unless ( $import ) {
627 unless ( qsearch('cust_main_county', {
628 'country' => $self->country,
631 return "Unknown state/county/country: ".
632 $self->state. "/". $self->county. "/". $self->country
633 unless qsearch('cust_main_county',{
634 'state' => $self->state,
635 'county' => $self->county,
636 'country' => $self->country,
642 $self->ut_phonen('daytime', $self->country)
643 || $self->ut_phonen('night', $self->country)
644 || $self->ut_phonen('fax', $self->country)
645 || $self->ut_zip('zip', $self->country)
647 return $error if $error;
650 last first company address1 address2 city county state zip
651 country daytime night fax
654 if ( defined $self->dbdef_table->column('ship_last') ) {
655 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
657 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
661 $self->ut_name('ship_last')
662 || $self->ut_name('ship_first')
663 || $self->ut_textn('ship_company')
664 || $self->ut_text('ship_address1')
665 || $self->ut_textn('ship_address2')
666 || $self->ut_text('ship_city')
667 || $self->ut_textn('ship_county')
668 || $self->ut_textn('ship_state')
669 || $self->ut_country('ship_country')
671 return $error if $error;
673 #false laziness with above
674 unless ( qsearchs('cust_main_county', {
675 'country' => $self->ship_country,
678 return "Unknown ship_state/ship_county/ship_country: ".
679 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
680 unless qsearchs('cust_main_county',{
681 'state' => $self->ship_state,
682 'county' => $self->ship_county,
683 'country' => $self->ship_country,
689 $self->ut_phonen('ship_daytime', $self->ship_country)
690 || $self->ut_phonen('ship_night', $self->ship_country)
691 || $self->ut_phonen('ship_fax', $self->ship_country)
692 || $self->ut_zip('ship_zip', $self->ship_country)
694 return $error if $error;
696 } else { # ship_ info eq billing info, so don't store dup info in database
697 $self->setfield("ship_$_", '')
698 foreach qw( last first company address1 address2 city county state zip
699 country daytime night fax );
703 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
704 or return "Illegal payby: ". $self->payby;
707 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
709 my $payinfo = $self->payinfo;
711 $payinfo =~ /^(\d{13,16})$/
712 or return gettext('invalid_card'); # . ": ". $self->payinfo;
714 $self->payinfo($payinfo);
716 or return gettext('invalid_card'); # . ": ". $self->payinfo;
717 return gettext('unknown_card_type')
718 if cardtype($self->payinfo) eq "Unknown";
720 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
722 my $payinfo = $self->payinfo;
723 $payinfo =~ s/[^\d\@]//g;
724 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
726 $self->payinfo($payinfo);
728 } elsif ( $self->payby eq 'LECB' ) {
730 my $payinfo = $self->payinfo;
732 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
734 $self->payinfo($payinfo);
736 } elsif ( $self->payby eq 'BILL' ) {
738 $error = $self->ut_textn('payinfo');
739 return "Illegal P.O. number: ". $self->payinfo if $error;
741 } elsif ( $self->payby eq 'COMP' ) {
743 $error = $self->ut_textn('payinfo');
744 return "Illegal comp account issuer: ". $self->payinfo if $error;
746 } elsif ( $self->payby eq 'PREPAY' ) {
748 my $payinfo = $self->payinfo;
749 $payinfo =~ s/\W//g; #anything else would just confuse things
750 $self->payinfo($payinfo);
751 $error = $self->ut_alpha('payinfo');
752 return "Illegal prepayment identifier: ". $self->payinfo if $error;
753 return "Unknown prepayment identifier"
754 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
758 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
759 return "Expriation date required"
760 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
763 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
764 or return "Illegal expiration date: ". $self->paydate;
765 my $y = length($2) == 4 ? $2 : "20$2";
766 $self->paydate("$y-$1-01");
767 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
768 return gettext('expired_card')
769 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
772 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
773 ( ! $conf->exists('require_cardname')
774 || $self->payby !~ /^(CARD|DCRD)$/ )
776 $self->payname( $self->first. " ". $self->getfield('last') );
778 $self->payname =~ /^([\w \,\.\-\']+)$/
779 or return gettext('illegal_name'). " payname: ". $self->payname;
783 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
786 $self->otaker(getotaker);
788 #warn "AFTER: \n". $self->_dump;
795 Returns all packages (see L<FS::cust_pkg>) for this customer.
801 if ( $self->{'_pkgnum'} ) {
802 values %{ $self->{'_pkgnum'}->cache };
804 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
808 =item ncancelled_pkgs
810 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
814 sub ncancelled_pkgs {
816 if ( $self->{'_pkgnum'} ) {
817 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
819 @{ [ # force list context
820 qsearch( 'cust_pkg', {
821 'custnum' => $self->custnum,
824 qsearch( 'cust_pkg', {
825 'custnum' => $self->custnum,
834 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
840 grep { $_->susp } $self->ncancelled_pkgs;
843 =item unflagged_suspended_pkgs
845 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
846 customer (thouse packages without the `manual_flag' set).
850 sub unflagged_suspended_pkgs {
852 return $self->suspended_pkgs
853 unless dbdef->table('cust_pkg')->column('manual_flag');
854 grep { ! $_->manual_flag } $self->suspended_pkgs;
857 =item unsuspended_pkgs
859 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
864 sub unsuspended_pkgs {
866 grep { ! $_->susp } $self->ncancelled_pkgs;
871 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
872 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
873 on success or a list of errors.
879 grep { $_->unsuspend } $self->suspended_pkgs;
884 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
885 Always returns a list: an empty list on success or a list of errors.
891 grep { $_->suspend } $self->unsuspended_pkgs;
896 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
897 Always returns a list: an empty list on success or a list of errors.
903 grep { $_->cancel } $self->ncancelled_pkgs;
908 Returns the agent (see L<FS::agent>) for this customer.
914 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
919 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
920 conjunction with the collect method.
922 Options are passed as name-value pairs.
924 The only currently available option is `time', which bills the customer as if
925 it were that time. It is specified as a UNIX timestamp; see
926 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
927 functions. For example:
931 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
933 If there is an error, returns the error, otherwise returns false.
938 my( $self, %options ) = @_;
939 my $time = $options{'time'} || time;
944 local $SIG{HUP} = 'IGNORE';
945 local $SIG{INT} = 'IGNORE';
946 local $SIG{QUIT} = 'IGNORE';
947 local $SIG{TERM} = 'IGNORE';
948 local $SIG{TSTP} = 'IGNORE';
949 local $SIG{PIPE} = 'IGNORE';
951 my $oldAutoCommit = $FS::UID::AutoCommit;
952 local $FS::UID::AutoCommit = 0;
955 # find the packages which are due for billing, find out how much they are
956 # & generate invoice database.
958 my( $total_setup, $total_recur ) = ( 0, 0 );
959 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
960 my @cust_bill_pkg = ();
962 #my $taxable_charged = 0;##
967 foreach my $cust_pkg (
968 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
971 #NO!! next if $cust_pkg->cancel;
972 next if $cust_pkg->getfield('cancel');
974 #? to avoid use of uninitialized value errors... ?
975 $cust_pkg->setfield('bill', '')
976 unless defined($cust_pkg->bill);
978 my $part_pkg = $cust_pkg->part_pkg;
980 #so we don't modify cust_pkg record unnecessarily
981 my $cust_pkg_mod_flag = 0;
982 my %hash = $cust_pkg->hash;
983 my $old_cust_pkg = new FS::cust_pkg \%hash;
989 unless ( $cust_pkg->setup ) {
990 my $setup_prog = $part_pkg->getfield('setup');
991 $setup_prog =~ /^(.*)$/ or do {
992 $dbh->rollback if $oldAutoCommit;
993 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
999 ##$cpt->permit(); #what is necessary?
1000 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1001 #$setup = $cpt->reval($setup_prog);
1002 $setup = eval $setup_prog;
1003 unless ( defined($setup) ) {
1004 $dbh->rollback if $oldAutoCommit;
1005 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1006 "(expression $setup_prog): $@";
1008 $cust_pkg->setfield('setup',$time);
1009 $cust_pkg_mod_flag=1;
1015 if ( $part_pkg->getfield('freq') > 0 &&
1016 ! $cust_pkg->getfield('susp') &&
1017 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1019 my $recur_prog = $part_pkg->getfield('recur');
1020 $recur_prog =~ /^(.*)$/ or do {
1021 $dbh->rollback if $oldAutoCommit;
1022 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1027 # shared with $recur_prog
1028 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1030 #my $cpt = new Safe;
1031 ##$cpt->permit(); #what is necessary?
1032 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1033 #$recur = $cpt->reval($recur_prog);
1034 $recur = eval $recur_prog;
1035 unless ( defined($recur) ) {
1036 $dbh->rollback if $oldAutoCommit;
1037 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1038 "(expression $recur_prog): $@";
1040 #change this bit to use Date::Manip? CAREFUL with timezones (see
1041 # mailing list archive)
1042 my ($sec,$min,$hour,$mday,$mon,$year) =
1043 (localtime($sdate) )[0,1,2,3,4,5];
1045 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1046 # only for figuring next bill date, nothing else, so, reset $sdate again
1048 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1049 $cust_pkg->last_bill($sdate)
1050 if $cust_pkg->dbdef_table->column('last_bill');
1052 $mon += $part_pkg->freq;
1053 until ( $mon < 12 ) { $mon -= 12; $year++; }
1054 $cust_pkg->setfield('bill',
1055 timelocal($sec,$min,$hour,$mday,$mon,$year));
1056 $cust_pkg_mod_flag = 1;
1059 warn "\$setup is undefined" unless defined($setup);
1060 warn "\$recur is undefined" unless defined($recur);
1061 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1063 my $taxable_charged = 0;
1064 if ( $cust_pkg_mod_flag ) {
1065 $error=$cust_pkg->replace($old_cust_pkg);
1066 if ( $error ) { #just in case
1067 $dbh->rollback if $oldAutoCommit;
1068 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1070 $setup = sprintf( "%.2f", $setup );
1071 $recur = sprintf( "%.2f", $recur );
1073 $dbh->rollback if $oldAutoCommit;
1074 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1077 $dbh->rollback if $oldAutoCommit;
1078 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1080 if ( $setup > 0 || $recur > 0 ) {
1081 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1082 'pkgnum' => $cust_pkg->pkgnum,
1086 'edate' => $cust_pkg->bill,
1087 'details' => \@details,
1089 push @cust_bill_pkg, $cust_bill_pkg;
1090 $total_setup += $setup;
1091 $total_recur += $recur;
1092 $taxable_charged += $setup
1093 unless $part_pkg->setuptax =~ /^Y$/i;
1094 $taxable_charged += $recur
1095 unless $part_pkg->recurtax =~ /^Y$/i;
1097 unless ( $self->tax =~ /Y/i
1098 || $self->payby eq 'COMP'
1099 || $taxable_charged == 0 ) {
1101 my $cust_main_county = qsearchs('cust_main_county',{
1102 'state' => $self->state,
1103 'county' => $self->county,
1104 'country' => $self->country,
1105 'taxclass' => $part_pkg->taxclass,
1107 $cust_main_county ||= qsearchs('cust_main_county',{
1108 'state' => $self->state,
1109 'county' => $self->county,
1110 'country' => $self->country,
1113 unless ( $cust_main_county ) {
1114 $dbh->rollback if $oldAutoCommit;
1116 "fatal: can't find tax rate for state/county/country/taxclass ".
1117 join('/', ( map $self->$_(), qw(state county country) ),
1118 $part_pkg->taxclass ). "\n";
1121 if ( $cust_main_county->exempt_amount ) {
1122 my ($mon,$year) = (localtime($sdate) )[4,5];
1124 my $freq = $part_pkg->freq || 1;
1125 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1126 foreach my $which_month ( 1 .. $freq ) {
1128 'custnum' => $self->custnum,
1129 'taxnum' => $cust_main_county->taxnum,
1130 'year' => 1900+$year,
1133 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1134 until ( $mon < 13 ) { $mon -= 12; $year++; }
1135 my $cust_tax_exempt =
1136 qsearchs('cust_tax_exempt', \%hash)
1137 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1138 my $remaining_exemption = sprintf("%.2f",
1139 $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
1140 if ( $remaining_exemption > 0 ) {
1141 my $addl = $remaining_exemption > $taxable_per_month
1142 ? $taxable_per_month
1143 : $remaining_exemption;
1144 $taxable_charged -= $addl;
1145 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1146 $cust_tax_exempt->hash,
1147 'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1149 $error = $new_cust_tax_exempt->exemptnum
1150 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1151 : $new_cust_tax_exempt->insert;
1153 $dbh->rollback if $oldAutoCommit;
1154 return "fatal: can't update cust_tax_exempt: $error";
1157 } # if $remaining_exemption > 0
1159 } #foreach $which_month
1161 } #if $cust_main_county->exempt_amount
1163 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1165 #$tax += $taxable_charged * $cust_main_county->tax / 100
1166 $tax{ $cust_main_county->taxname || 'Tax' } +=
1167 $taxable_charged * $cust_main_county->tax / 100
1169 } #unless $self->tax =~ /Y/i
1170 # || $self->payby eq 'COMP'
1171 # || $taxable_charged == 0
1173 } #if $setup > 0 || $recur > 0
1175 } #if $cust_pkg_mod_flag
1177 } #foreach my $cust_pkg
1179 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1180 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1182 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1183 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1187 # unless ( $self->tax =~ /Y/i
1188 # || $self->payby eq 'COMP'
1189 # || $taxable_charged == 0 ) {
1190 # my $cust_main_county = qsearchs('cust_main_county',{
1191 # 'state' => $self->state,
1192 # 'county' => $self->county,
1193 # 'country' => $self->country,
1194 # } ) or die "fatal: can't find tax rate for state/county/country ".
1195 # $self->state. "/". $self->county. "/". $self->country. "\n";
1196 # my $tax = sprintf( "%.2f",
1197 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1200 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1201 my $tax = sprintf("%.2f", $tax{$taxname} );
1202 $charged = sprintf( "%.2f", $charged+$tax );
1204 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1210 'itemdesc' => $taxname,
1212 push @cust_bill_pkg, $cust_bill_pkg;
1216 my $cust_bill = new FS::cust_bill ( {
1217 'custnum' => $self->custnum,
1219 'charged' => $charged,
1221 $error = $cust_bill->insert;
1223 $dbh->rollback if $oldAutoCommit;
1224 return "can't create invoice for customer #". $self->custnum. ": $error";
1227 my $invnum = $cust_bill->invnum;
1229 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1231 $cust_bill_pkg->invnum($invnum);
1232 $error = $cust_bill_pkg->insert;
1234 $dbh->rollback if $oldAutoCommit;
1235 return "can't create invoice line item for customer #". $self->custnum.
1240 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1244 =item collect OPTIONS
1246 (Attempt to) collect money for this customer's outstanding invoices (see
1247 L<FS::cust_bill>). Usually used after the bill method.
1249 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1250 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1251 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1253 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1254 and the invoice events web interface.
1256 If there is an error, returns the error, otherwise returns false.
1258 Options are passed as name-value pairs.
1260 Currently available options are:
1262 invoice_time - Use this time when deciding when to print invoices and
1263 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>
1264 for conversion functions.
1266 retry_card - Retry cards even when not scheduled by invoice events.
1268 batch_card - This option is deprecated. See the invoice events web interface
1269 to control whether cards are batched or run against a realtime gateway.
1271 report_badcard - This option is deprecated.
1273 force_print - This option is deprecated; see the invoice events web interface.
1278 my( $self, %options ) = @_;
1279 my $invoice_time = $options{'invoice_time'} || time;
1282 local $SIG{HUP} = 'IGNORE';
1283 local $SIG{INT} = 'IGNORE';
1284 local $SIG{QUIT} = 'IGNORE';
1285 local $SIG{TERM} = 'IGNORE';
1286 local $SIG{TSTP} = 'IGNORE';
1287 local $SIG{PIPE} = 'IGNORE';
1289 my $oldAutoCommit = $FS::UID::AutoCommit;
1290 local $FS::UID::AutoCommit = 0;
1293 my $balance = $self->balance;
1294 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1295 unless ( $balance > 0 ) { #redundant?????
1296 $dbh->rollback if $oldAutoCommit; #hmm
1300 if ( exists($options{'retry_card'}) && $options{'retry_card'} ) {
1301 #false laziness w/replace
1302 foreach my $cust_bill_event (
1304 #$_->part_bill_event->plan eq 'realtime-card'
1305 $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
1306 && $_->status eq 'done'
1309 map { $_->cust_bill_event }
1310 grep { $_->cust_bill_event }
1311 $self->open_cust_bill
1313 my $error = $cust_bill_event->retry;
1315 $dbh->rollback if $oldAutoCommit;
1316 return "error scheduling invoice events for retry: $error";
1322 foreach my $cust_bill ( $self->cust_bill ) {
1324 #this has to be before next's
1325 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1329 $balance = sprintf( "%.2f", $balance - $amount );
1331 next unless $cust_bill->owed > 0;
1333 # don't try to charge for the same invoice if it's already in a batch
1334 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1336 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1338 next unless $amount > 0;
1341 foreach my $part_bill_event (
1342 sort { $a->seconds <=> $b->seconds
1343 || $a->weight <=> $b->weight
1344 || $a->eventpart <=> $b->eventpart }
1345 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1346 && ! qsearchs( 'cust_bill_event', {
1347 'invnum' => $cust_bill->invnum,
1348 'eventpart' => $_->eventpart,
1352 qsearch('part_bill_event', { 'payby' => $self->payby,
1353 'disabled' => '', } )
1356 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1358 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1360 my $cust_main = $self; #for callback
1361 my $error = eval $part_bill_event->eventcode;
1364 my $statustext = '';
1368 } elsif ( $error ) {
1370 $statustext = $error;
1375 #add cust_bill_event
1376 my $cust_bill_event = new FS::cust_bill_event {
1377 'invnum' => $cust_bill->invnum,
1378 'eventpart' => $part_bill_event->eventpart,
1379 #'_date' => $invoice_time,
1381 'status' => $status,
1382 'statustext' => $statustext,
1384 $error = $cust_bill_event->insert;
1386 #$dbh->rollback if $oldAutoCommit;
1387 #return "error: $error";
1389 # gah, even with transactions.
1390 $dbh->commit if $oldAutoCommit; #well.
1391 my $e = 'WARNING: Event run but database not updated - '.
1392 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1393 ', eventpart '. $part_bill_event->eventpart.
1404 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1411 Returns the total owed for this customer on all invoices
1412 (see L<FS::cust_bill/owed>).
1418 $self->total_owed_date(2145859200); #12/31/2037
1421 =item total_owed_date TIME
1423 Returns the total owed for this customer on all invoices with date earlier than
1424 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1425 see L<Time::Local> and L<Date::Parse> for conversion functions.
1429 sub total_owed_date {
1433 foreach my $cust_bill (
1434 grep { $_->_date <= $time }
1435 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1437 $total_bill += $cust_bill->owed;
1439 sprintf( "%.2f", $total_bill );
1444 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1445 to outstanding invoice balances in chronological order and returns the value
1446 of any remaining unapplied credits available for refund
1447 (see L<FS::cust_refund>).
1454 return 0 unless $self->total_credited;
1456 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1457 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1459 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1460 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1464 foreach my $cust_bill ( @invoices ) {
1467 if ( !defined($credit) || $credit->credited == 0) {
1468 $credit = pop @credits or last;
1471 if ($cust_bill->owed >= $credit->credited) {
1472 $amount=$credit->credited;
1474 $amount=$cust_bill->owed;
1477 my $cust_credit_bill = new FS::cust_credit_bill ( {
1478 'crednum' => $credit->crednum,
1479 'invnum' => $cust_bill->invnum,
1480 'amount' => $amount,
1482 my $error = $cust_credit_bill->insert;
1483 die $error if $error;
1485 redo if ($cust_bill->owed > 0);
1489 return $self->total_credited;
1492 =item apply_payments
1494 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1495 to outstanding invoice balances in chronological order.
1497 #and returns the value of any remaining unapplied payments.
1501 sub apply_payments {
1506 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1507 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1509 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1510 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1514 foreach my $cust_bill ( @invoices ) {
1517 if ( !defined($payment) || $payment->unapplied == 0 ) {
1518 $payment = pop @payments or last;
1521 if ( $cust_bill->owed >= $payment->unapplied ) {
1522 $amount = $payment->unapplied;
1524 $amount = $cust_bill->owed;
1527 my $cust_bill_pay = new FS::cust_bill_pay ( {
1528 'paynum' => $payment->paynum,
1529 'invnum' => $cust_bill->invnum,
1530 'amount' => $amount,
1532 my $error = $cust_bill_pay->insert;
1533 die $error if $error;
1535 redo if ( $cust_bill->owed > 0);
1539 return $self->total_unapplied_payments;
1542 =item total_credited
1544 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1545 customer. See L<FS::cust_credit/credited>.
1549 sub total_credited {
1551 my $total_credit = 0;
1552 foreach my $cust_credit ( qsearch('cust_credit', {
1553 'custnum' => $self->custnum,
1555 $total_credit += $cust_credit->credited;
1557 sprintf( "%.2f", $total_credit );
1560 =item total_unapplied_payments
1562 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1563 See L<FS::cust_pay/unapplied>.
1567 sub total_unapplied_payments {
1569 my $total_unapplied = 0;
1570 foreach my $cust_pay ( qsearch('cust_pay', {
1571 'custnum' => $self->custnum,
1573 $total_unapplied += $cust_pay->unapplied;
1575 sprintf( "%.2f", $total_unapplied );
1580 Returns the balance for this customer (total_owed minus total_credited
1581 minus total_unapplied_payments).
1588 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1592 =item balance_date TIME
1594 Returns the balance for this customer, only considering invoices with date
1595 earlier than TIME (total_owed_date minus total_credited minus
1596 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1597 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1606 $self->total_owed_date($time)
1607 - $self->total_credited
1608 - $self->total_unapplied_payments
1612 =item invoicing_list [ ARRAYREF ]
1614 If an arguement is given, sets these email addresses as invoice recipients
1615 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1616 (except as warnings), so use check_invoicing_list first.
1618 Returns a list of email addresses (with svcnum entries expanded).
1620 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1621 check it without disturbing anything by passing nothing.
1623 This interface may change in the future.
1627 sub invoicing_list {
1628 my( $self, $arrayref ) = @_;
1630 my @cust_main_invoice;
1631 if ( $self->custnum ) {
1632 @cust_main_invoice =
1633 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1635 @cust_main_invoice = ();
1637 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1638 #warn $cust_main_invoice->destnum;
1639 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1640 #warn $cust_main_invoice->destnum;
1641 my $error = $cust_main_invoice->delete;
1642 warn $error if $error;
1645 if ( $self->custnum ) {
1646 @cust_main_invoice =
1647 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1649 @cust_main_invoice = ();
1651 my %seen = map { $_->address => 1 } @cust_main_invoice;
1652 foreach my $address ( @{$arrayref} ) {
1653 next if exists $seen{$address} && $seen{$address};
1654 $seen{$address} = 1;
1655 my $cust_main_invoice = new FS::cust_main_invoice ( {
1656 'custnum' => $self->custnum,
1659 my $error = $cust_main_invoice->insert;
1660 warn $error if $error;
1663 if ( $self->custnum ) {
1665 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1671 =item check_invoicing_list ARRAYREF
1673 Checks these arguements as valid input for the invoicing_list method. If there
1674 is an error, returns the error, otherwise returns false.
1678 sub check_invoicing_list {
1679 my( $self, $arrayref ) = @_;
1680 foreach my $address ( @{$arrayref} ) {
1681 my $cust_main_invoice = new FS::cust_main_invoice ( {
1682 'custnum' => $self->custnum,
1685 my $error = $self->custnum
1686 ? $cust_main_invoice->check
1687 : $cust_main_invoice->checkdest
1689 return $error if $error;
1694 =item set_default_invoicing_list
1696 Sets the invoicing list to all accounts associated with this customer,
1697 overwriting any previous invoicing list.
1701 sub set_default_invoicing_list {
1703 $self->invoicing_list($self->all_emails);
1708 Returns the email addresses of all accounts provisioned for this customer.
1715 foreach my $cust_pkg ( $self->all_pkgs ) {
1716 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1718 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1719 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1721 $list{$_}=1 foreach map { $_->email } @svc_acct;
1726 =item invoicing_list_addpost
1728 Adds postal invoicing to this customer. If this customer is already configured
1729 to receive postal invoices, does nothing.
1733 sub invoicing_list_addpost {
1735 return if grep { $_ eq 'POST' } $self->invoicing_list;
1736 my @invoicing_list = $self->invoicing_list;
1737 push @invoicing_list, 'POST';
1738 $self->invoicing_list(\@invoicing_list);
1741 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1743 Returns an array of customers referred by this customer (referral_custnum set
1744 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1745 customers referred by customers referred by this customer and so on, inclusive.
1746 The default behavior is DEPTH 1 (no recursion).
1750 sub referral_cust_main {
1752 my $depth = @_ ? shift : 1;
1753 my $exclude = @_ ? shift : {};
1756 map { $exclude->{$_->custnum}++; $_; }
1757 grep { ! $exclude->{ $_->custnum } }
1758 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1762 map { $_->referral_cust_main($depth-1, $exclude) }
1769 =item referral_cust_main_ncancelled
1771 Same as referral_cust_main, except only returns customers with uncancelled
1776 sub referral_cust_main_ncancelled {
1778 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
1781 =item referral_cust_pkg [ DEPTH ]
1783 Like referral_cust_main, except returns a flat list of all unsuspended (and
1784 uncancelled) packages for each customer. The number of items in this list may
1785 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
1789 sub referral_cust_pkg {
1791 my $depth = @_ ? shift : 1;
1793 map { $_->unsuspended_pkgs }
1794 grep { $_->unsuspended_pkgs }
1795 $self->referral_cust_main($depth);
1798 =item credit AMOUNT, REASON
1800 Applies a credit to this customer. If there is an error, returns the error,
1801 otherwise returns false.
1806 my( $self, $amount, $reason ) = @_;
1807 my $cust_credit = new FS::cust_credit {
1808 'custnum' => $self->custnum,
1809 'amount' => $amount,
1810 'reason' => $reason,
1812 $cust_credit->insert;
1815 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
1817 Creates a one-time charge for this customer. If there is an error, returns
1818 the error, otherwise returns false.
1823 my ( $self, $amount ) = ( shift, shift );
1824 my $pkg = @_ ? shift : 'One-time charge';
1825 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
1826 my $taxclass = @_ ? shift : '';
1828 local $SIG{HUP} = 'IGNORE';
1829 local $SIG{INT} = 'IGNORE';
1830 local $SIG{QUIT} = 'IGNORE';
1831 local $SIG{TERM} = 'IGNORE';
1832 local $SIG{TSTP} = 'IGNORE';
1833 local $SIG{PIPE} = 'IGNORE';
1835 my $oldAutoCommit = $FS::UID::AutoCommit;
1836 local $FS::UID::AutoCommit = 0;
1839 my $part_pkg = new FS::part_pkg ( {
1841 'comment' => $comment,
1846 'taxclass' => $taxclass,
1849 my $error = $part_pkg->insert;
1851 $dbh->rollback if $oldAutoCommit;
1855 my $pkgpart = $part_pkg->pkgpart;
1856 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
1857 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
1858 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
1859 $error = $type_pkgs->insert;
1861 $dbh->rollback if $oldAutoCommit;
1866 my $cust_pkg = new FS::cust_pkg ( {
1867 'custnum' => $self->custnum,
1868 'pkgpart' => $pkgpart,
1871 $error = $cust_pkg->insert;
1873 $dbh->rollback if $oldAutoCommit;
1877 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1884 Returns all the invoices (see L<FS::cust_bill>) for this customer.
1890 sort { $a->_date <=> $b->_date }
1891 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1894 =item open_cust_bill
1896 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
1901 sub open_cust_bill {
1903 grep { $_->owed > 0 } $self->cust_bill;
1912 =item check_and_rebuild_fuzzyfiles
1916 sub check_and_rebuild_fuzzyfiles {
1917 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1918 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1919 or &rebuild_fuzzyfiles;
1922 =item rebuild_fuzzyfiles
1926 sub rebuild_fuzzyfiles {
1928 use Fcntl qw(:flock);
1930 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1934 open(LASTLOCK,">>$dir/cust_main.last")
1935 or die "can't open $dir/cust_main.last: $!";
1936 flock(LASTLOCK,LOCK_EX)
1937 or die "can't lock $dir/cust_main.last: $!";
1939 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1941 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1942 if defined dbdef->table('cust_main')->column('ship_last');
1944 open (LASTCACHE,">$dir/cust_main.last.tmp")
1945 or die "can't open $dir/cust_main.last.tmp: $!";
1946 print LASTCACHE join("\n", @all_last), "\n";
1947 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1949 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1954 open(COMPANYLOCK,">>$dir/cust_main.company")
1955 or die "can't open $dir/cust_main.company: $!";
1956 flock(COMPANYLOCK,LOCK_EX)
1957 or die "can't lock $dir/cust_main.company: $!";
1959 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1961 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1962 if defined dbdef->table('cust_main')->column('ship_last');
1964 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1965 or die "can't open $dir/cust_main.company.tmp: $!";
1966 print COMPANYCACHE join("\n", @all_company), "\n";
1967 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1969 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1979 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1980 open(LASTCACHE,"<$dir/cust_main.last")
1981 or die "can't open $dir/cust_main.last: $!";
1982 my @array = map { chomp; $_; } <LASTCACHE>;
1992 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1993 open(COMPANYCACHE,"<$dir/cust_main.company")
1994 or die "can't open $dir/cust_main.last: $!";
1995 my @array = map { chomp; $_; } <COMPANYCACHE>;
2000 =item append_fuzzyfiles LASTNAME COMPANY
2004 sub append_fuzzyfiles {
2005 my( $last, $company ) = @_;
2007 &check_and_rebuild_fuzzyfiles;
2009 use Fcntl qw(:flock);
2011 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2015 open(LAST,">>$dir/cust_main.last")
2016 or die "can't open $dir/cust_main.last: $!";
2018 or die "can't lock $dir/cust_main.last: $!";
2020 print LAST "$last\n";
2023 or die "can't unlock $dir/cust_main.last: $!";
2029 open(COMPANY,">>$dir/cust_main.company")
2030 or die "can't open $dir/cust_main.company: $!";
2031 flock(COMPANY,LOCK_EX)
2032 or die "can't lock $dir/cust_main.company: $!";
2034 print COMPANY "$company\n";
2036 flock(COMPANY,LOCK_UN)
2037 or die "can't unlock $dir/cust_main.company: $!";
2051 #warn join('-',keys %$param);
2052 my $fh = $param->{filehandle};
2053 my $agentnum = $param->{agentnum};
2054 my $refnum = $param->{refnum};
2055 my $pkgpart = $param->{pkgpart};
2056 my @fields = @{$param->{fields}};
2058 eval "use Date::Parse;";
2060 eval "use Text::CSV_XS;";
2063 my $csv = new Text::CSV_XS;
2070 local $SIG{HUP} = 'IGNORE';
2071 local $SIG{INT} = 'IGNORE';
2072 local $SIG{QUIT} = 'IGNORE';
2073 local $SIG{TERM} = 'IGNORE';
2074 local $SIG{TSTP} = 'IGNORE';
2075 local $SIG{PIPE} = 'IGNORE';
2077 my $oldAutoCommit = $FS::UID::AutoCommit;
2078 local $FS::UID::AutoCommit = 0;
2081 #while ( $columns = $csv->getline($fh) ) {
2083 while ( defined($line=<$fh>) ) {
2085 $csv->parse($line) or do {
2086 $dbh->rollback if $oldAutoCommit;
2087 return "can't parse: ". $csv->error_input();
2090 my @columns = $csv->fields();
2091 #warn join('-',@columns);
2094 agentnum => $agentnum,
2096 country => 'US', #default
2097 payby => 'BILL', #default
2098 paydate => '12/2037', #default
2100 my $billtime = time;
2101 my %cust_pkg = ( pkgpart => $pkgpart );
2102 foreach my $field ( @fields ) {
2103 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2104 #$cust_pkg{$1} = str2time( shift @$columns );
2105 if ( $1 eq 'setup' ) {
2106 $billtime = str2time(shift @columns);
2108 $cust_pkg{$1} = str2time( shift @columns );
2111 #$cust_main{$field} = shift @$columns;
2112 $cust_main{$field} = shift @columns;
2116 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2117 my $cust_main = new FS::cust_main ( \%cust_main );
2119 tie my %hash, 'Tie::RefHash'; #this part is important
2120 $hash{$cust_pkg} = [] if $pkgpart;
2121 my $error = $cust_main->insert( \%hash );
2124 $dbh->rollback if $oldAutoCommit;
2125 return "can't insert customer for $line: $error";
2128 #false laziness w/bill.cgi
2129 $error = $cust_main->bill( 'time' => $billtime );
2131 $dbh->rollback if $oldAutoCommit;
2132 return "can't bill customer for $line: $error";
2135 $cust_main->apply_payments;
2136 $cust_main->apply_credits;
2138 $error = $cust_main->collect();
2140 $dbh->rollback if $oldAutoCommit;
2141 return "can't collect customer for $line: $error";
2147 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2149 return "Empty file!" unless $imported;
2161 #warn join('-',keys %$param);
2162 my $fh = $param->{filehandle};
2163 my @fields = @{$param->{fields}};
2165 eval "use Date::Parse;";
2167 eval "use Text::CSV_XS;";
2170 my $csv = new Text::CSV_XS;
2177 local $SIG{HUP} = 'IGNORE';
2178 local $SIG{INT} = 'IGNORE';
2179 local $SIG{QUIT} = 'IGNORE';
2180 local $SIG{TERM} = 'IGNORE';
2181 local $SIG{TSTP} = 'IGNORE';
2182 local $SIG{PIPE} = 'IGNORE';
2184 my $oldAutoCommit = $FS::UID::AutoCommit;
2185 local $FS::UID::AutoCommit = 0;
2188 #while ( $columns = $csv->getline($fh) ) {
2190 while ( defined($line=<$fh>) ) {
2192 $csv->parse($line) or do {
2193 $dbh->rollback if $oldAutoCommit;
2194 return "can't parse: ". $csv->error_input();
2197 my @columns = $csv->fields();
2198 #warn join('-',@columns);
2201 foreach my $field ( @fields ) {
2202 $row{$field} = shift @columns;
2205 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2206 unless ( $cust_main ) {
2207 $dbh->rollback if $oldAutoCommit;
2208 return "unknown custnum $row{'custnum'}";
2211 if ( $row{'amount'} > 0 ) {
2212 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2214 $dbh->rollback if $oldAutoCommit;
2218 } elsif ( $row{'amount'} < 0 ) {
2219 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2222 $dbh->rollback if $oldAutoCommit;
2232 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2234 return "Empty file!" unless $imported;
2246 The delete method should possibly take an FS::cust_main object reference
2247 instead of a scalar customer number.
2249 Bill and collect options should probably be passed as references instead of a
2252 There should probably be a configuration file with a list of allowed credit
2255 No multiple currency support (probably a larger project than just this module).
2259 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2260 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2261 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.