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 {
43 #yes, need it for stuff below (prolly should be cached)
48 my ( $hashref, $cache ) = @_;
49 if ( exists $hashref->{'pkgnum'} ) {
50 # #@{ $self->{'_pkgnum'} } = ();
51 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
52 $self->{'_pkgnum'} = $subcache;
53 #push @{ $self->{'_pkgnum'} },
54 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
60 FS::cust_main - Object methods for cust_main records
66 $record = new FS::cust_main \%hash;
67 $record = new FS::cust_main { 'column' => 'value' };
69 $error = $record->insert;
71 $error = $new_record->replace($old_record);
73 $error = $record->delete;
75 $error = $record->check;
77 @cust_pkg = $record->all_pkgs;
79 @cust_pkg = $record->ncancelled_pkgs;
81 @cust_pkg = $record->suspended_pkgs;
83 $error = $record->bill;
84 $error = $record->bill %options;
85 $error = $record->bill 'time' => $time;
87 $error = $record->collect;
88 $error = $record->collect %options;
89 $error = $record->collect 'invoice_time' => $time,
90 'batch_card' => 'yes',
91 'report_badcard' => 'yes',
96 An FS::cust_main object represents a customer. FS::cust_main inherits from
97 FS::Record. The following fields are currently supported:
101 =item custnum - primary key (assigned automatically for new customers)
103 =item agentnum - agent (see L<FS::agent>)
105 =item refnum - Advertising source (see L<FS::part_referral>)
111 =item ss - social security number (optional)
113 =item company - (optional)
117 =item address2 - (optional)
121 =item county - (optional, see L<FS::cust_main_county>)
123 =item state - (see L<FS::cust_main_county>)
127 =item country - (see L<FS::cust_main_county>)
129 =item daytime - phone (optional)
131 =item night - phone (optional)
133 =item fax - phone (optional)
135 =item ship_first - name
137 =item ship_last - name
139 =item ship_company - (optional)
143 =item ship_address2 - (optional)
147 =item ship_county - (optional, see L<FS::cust_main_county>)
149 =item ship_state - (see L<FS::cust_main_county>)
153 =item ship_country - (see L<FS::cust_main_county>)
155 =item ship_daytime - phone (optional)
157 =item ship_night - phone (optional)
159 =item ship_fax - phone (optional)
161 =item payby - `CARD' (credit cards), `CHEK' (electronic check), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
163 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
165 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
167 =item payname - name on card or billing name
169 =item tax - tax exempt, empty or `Y'
171 =item otaker - order taker (assigned automatically, see L<FS::UID>)
173 =item comments - comments (optional)
183 Creates a new customer. To add the customer to the database, see L<"insert">.
185 Note that this stores the hash reference, not a distinct copy of the hash it
186 points to. You can ask the object for a copy with the I<hash> method.
190 sub table { 'cust_main'; }
192 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
194 Adds this customer to the database. If there is an error, returns the error,
195 otherwise returns false.
197 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
198 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
199 are inserted atomicly, or the transaction is rolled back. Passing an empty
200 hash reference is equivalent to not supplying this parameter. There should be
201 a better explanation of this, but until then, here's an example:
204 tie %hash, 'Tie::RefHash'; #this part is important
206 $cust_pkg => [ $svc_acct ],
209 $cust_main->insert( \%hash );
211 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
212 be set as the invoicing list (see L<"invoicing_list">). Errors return as
213 expected and rollback the entire transaction; it is not necessary to call
214 check_invoicing_list first. The invoicing_list is set after the records in the
215 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
216 invoicing_list destination to the newly-created svc_acct. Here's an example:
218 $cust_main->insert( {}, [ $email, 'POST' ] );
224 my $cust_pkgs = @_ ? shift : {};
225 my $invoicing_list = @_ ? shift : '';
227 local $SIG{HUP} = 'IGNORE';
228 local $SIG{INT} = 'IGNORE';
229 local $SIG{QUIT} = 'IGNORE';
230 local $SIG{TERM} = 'IGNORE';
231 local $SIG{TSTP} = 'IGNORE';
232 local $SIG{PIPE} = 'IGNORE';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
240 if ( $self->payby eq 'PREPAY' ) {
241 $self->payby('BILL');
242 my $prepay_credit = qsearchs(
244 { 'identifier' => $self->payinfo },
248 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
249 unless $prepay_credit;
250 $amount = $prepay_credit->amount;
251 $seconds = $prepay_credit->seconds;
252 my $error = $prepay_credit->delete;
254 $dbh->rollback if $oldAutoCommit;
255 return "removing prepay_credit (transaction rolled back): $error";
259 my $error = $self->SUPER::insert;
261 $dbh->rollback if $oldAutoCommit;
262 #return "inserting cust_main record (transaction rolled back): $error";
267 if ( $invoicing_list ) {
268 $error = $self->check_invoicing_list( $invoicing_list );
270 $dbh->rollback if $oldAutoCommit;
271 return "checking invoicing_list (transaction rolled back): $error";
273 $self->invoicing_list( $invoicing_list );
277 foreach my $cust_pkg ( keys %$cust_pkgs ) {
278 $cust_pkg->custnum( $self->custnum );
279 $error = $cust_pkg->insert;
281 $dbh->rollback if $oldAutoCommit;
282 return "inserting cust_pkg (transaction rolled back): $error";
284 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
285 $svc_something->pkgnum( $cust_pkg->pkgnum );
286 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
287 $svc_something->seconds( $svc_something->seconds + $seconds );
290 $error = $svc_something->insert;
292 $dbh->rollback if $oldAutoCommit;
293 #return "inserting svc_ (transaction rolled back): $error";
300 $dbh->rollback if $oldAutoCommit;
301 return "No svc_acct record to apply pre-paid time";
305 my $cust_credit = new FS::cust_credit {
306 'custnum' => $self->custnum,
309 $error = $cust_credit->insert;
311 $dbh->rollback if $oldAutoCommit;
312 return "inserting credit (transaction rolled back): $error";
316 #false laziness with sub replace
317 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
318 $error = $queue->insert($self->getfield('last'), $self->company);
320 $dbh->rollback if $oldAutoCommit;
321 return "queueing job (transaction rolled back): $error";
324 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
325 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
326 $error = $queue->insert($self->getfield('last'), $self->company);
328 $dbh->rollback if $oldAutoCommit;
329 return "queueing job (transaction rolled back): $error";
334 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
339 =item delete NEW_CUSTNUM
341 This deletes the customer. If there is an error, returns the error, otherwise
344 This will completely remove all traces of the customer record. This is not
345 what you want when a customer cancels service; for that, cancel all of the
346 customer's packages (see L<FS::cust_pkg/cancel>).
348 If the customer has any uncancelled packages, you need to pass a new (valid)
349 customer number for those packages to be transferred to. Cancelled packages
350 will be deleted. Did I mention that this is NOT what you want when a customer
351 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
353 You can't delete a customer with invoices (see L<FS::cust_bill>),
354 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
355 refunds (see L<FS::cust_refund>).
362 local $SIG{HUP} = 'IGNORE';
363 local $SIG{INT} = 'IGNORE';
364 local $SIG{QUIT} = 'IGNORE';
365 local $SIG{TERM} = 'IGNORE';
366 local $SIG{TSTP} = 'IGNORE';
367 local $SIG{PIPE} = 'IGNORE';
369 my $oldAutoCommit = $FS::UID::AutoCommit;
370 local $FS::UID::AutoCommit = 0;
373 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
374 $dbh->rollback if $oldAutoCommit;
375 return "Can't delete a customer with invoices";
377 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
378 $dbh->rollback if $oldAutoCommit;
379 return "Can't delete a customer with credits";
381 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
382 $dbh->rollback if $oldAutoCommit;
383 return "Can't delete a customer with payments";
385 if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
386 $dbh->rollback if $oldAutoCommit;
387 return "Can't delete a customer with refunds";
390 my @cust_pkg = $self->ncancelled_pkgs;
392 my $new_custnum = shift;
393 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
394 $dbh->rollback if $oldAutoCommit;
395 return "Invalid new customer number: $new_custnum";
397 foreach my $cust_pkg ( @cust_pkg ) {
398 my %hash = $cust_pkg->hash;
399 $hash{'custnum'} = $new_custnum;
400 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
401 my $error = $new_cust_pkg->replace($cust_pkg);
403 $dbh->rollback if $oldAutoCommit;
408 my @cancelled_cust_pkg = $self->all_pkgs;
409 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
410 my $error = $cust_pkg->delete;
412 $dbh->rollback if $oldAutoCommit;
417 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
418 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
420 my $error = $cust_main_invoice->delete;
422 $dbh->rollback if $oldAutoCommit;
427 my $error = $self->SUPER::delete;
429 $dbh->rollback if $oldAutoCommit;
433 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
438 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
440 Replaces the OLD_RECORD with this one in the database. If there is an error,
441 returns the error, otherwise returns false.
443 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
444 be set as the invoicing list (see L<"invoicing_list">). Errors return as
445 expected and rollback the entire transaction; it is not necessary to call
446 check_invoicing_list first. Here's an example:
448 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
457 local $SIG{HUP} = 'IGNORE';
458 local $SIG{INT} = 'IGNORE';
459 local $SIG{QUIT} = 'IGNORE';
460 local $SIG{TERM} = 'IGNORE';
461 local $SIG{TSTP} = 'IGNORE';
462 local $SIG{PIPE} = 'IGNORE';
464 my $oldAutoCommit = $FS::UID::AutoCommit;
465 local $FS::UID::AutoCommit = 0;
468 my $error = $self->SUPER::replace($old);
471 $dbh->rollback if $oldAutoCommit;
475 if ( @param ) { # INVOICING_LIST_ARYREF
476 my $invoicing_list = shift @param;
477 $error = $self->check_invoicing_list( $invoicing_list );
479 $dbh->rollback if $oldAutoCommit;
482 $self->invoicing_list( $invoicing_list );
485 if ( $self->payby =~ /^(CARD|CHEK)$/ &&
486 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
487 # card info has changed, want to retry realtime_card invoice events
488 #false laziness w/collect
489 foreach my $cust_bill_event (
491 #$_->part_bill_event->plan eq 'realtime-card'
492 $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
493 && $_->status eq 'done'
496 map { $_->cust_bill_event }
497 grep { $_->cust_bill_event }
498 $self->open_cust_bill
501 my $error = $cust_bill_event->retry;
503 $dbh->rollback if $oldAutoCommit;
504 return "error scheduling invoice events for retry: $error";
511 #false laziness with sub insert
512 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
513 $error = $queue->insert($self->getfield('last'), $self->company);
515 $dbh->rollback if $oldAutoCommit;
516 return "queueing job (transaction rolled back): $error";
519 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
520 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
521 $error = $queue->insert($self->getfield('last'), $self->company);
523 $dbh->rollback if $oldAutoCommit;
524 return "queueing job (transaction rolled back): $error";
529 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
536 Checks all fields to make sure this is a valid customer record. If there is
537 an error, returns the error, otherwise returns false. Called by the insert
545 #warn "BEFORE: \n". $self->_dump;
548 $self->ut_numbern('custnum')
549 || $self->ut_number('agentnum')
550 || $self->ut_number('refnum')
551 || $self->ut_name('last')
552 || $self->ut_name('first')
553 || $self->ut_textn('company')
554 || $self->ut_text('address1')
555 || $self->ut_textn('address2')
556 || $self->ut_text('city')
557 || $self->ut_textn('county')
558 || $self->ut_textn('state')
559 || $self->ut_country('country')
560 || $self->ut_anything('comments')
561 || $self->ut_numbern('referral_custnum')
563 #barf. need message catalogs. i18n. etc.
564 $error .= "Please select a advertising source."
565 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
566 return $error if $error;
568 return "Unknown agent"
569 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
571 return "Unknown refnum"
572 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
574 return "Unknown referring custnum ". $self->referral_custnum
575 unless ! $self->referral_custnum
576 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
578 if ( $self->ss eq '' ) {
583 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
584 or return "Illegal social security number: ". $self->ss;
585 $self->ss("$1-$2-$3");
589 # bad idea to disable, causes billing to fail because of no tax rates later
590 # unless ( $import ) {
591 unless ( qsearchs('cust_main_county', {
592 'country' => $self->country,
595 return "Unknown state/county/country: ".
596 $self->state. "/". $self->county. "/". $self->country
597 unless qsearchs('cust_main_county',{
598 'state' => $self->state,
599 'county' => $self->county,
600 'country' => $self->country,
606 $self->ut_phonen('daytime', $self->country)
607 || $self->ut_phonen('night', $self->country)
608 || $self->ut_phonen('fax', $self->country)
609 || $self->ut_zip('zip', $self->country)
611 return $error if $error;
614 last first company address1 address2 city county state zip
615 country daytime night fax
618 if ( defined $self->dbdef_table->column('ship_last') ) {
619 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
621 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
625 $self->ut_name('ship_last')
626 || $self->ut_name('ship_first')
627 || $self->ut_textn('ship_company')
628 || $self->ut_text('ship_address1')
629 || $self->ut_textn('ship_address2')
630 || $self->ut_text('ship_city')
631 || $self->ut_textn('ship_county')
632 || $self->ut_textn('ship_state')
633 || $self->ut_country('ship_country')
635 return $error if $error;
637 #false laziness with above
638 unless ( qsearchs('cust_main_county', {
639 'country' => $self->ship_country,
642 return "Unknown ship_state/ship_county/ship_country: ".
643 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
644 unless qsearchs('cust_main_county',{
645 'state' => $self->ship_state,
646 'county' => $self->ship_county,
647 'country' => $self->ship_country,
653 $self->ut_phonen('ship_daytime', $self->ship_country)
654 || $self->ut_phonen('ship_night', $self->ship_country)
655 || $self->ut_phonen('ship_fax', $self->ship_country)
656 || $self->ut_zip('ship_zip', $self->ship_country)
658 return $error if $error;
660 } else { # ship_ info eq billing info, so don't store dup info in database
661 $self->setfield("ship_$_", '')
662 foreach qw( last first company address1 address2 city county state zip
663 country daytime night fax );
667 $self->payby =~ /^(CARD|CHEK|BILL|COMP|PREPAY)$/
668 or return "Illegal payby: ". $self->payby;
671 if ( $self->payby eq 'CARD' ) {
673 my $payinfo = $self->payinfo;
675 $payinfo =~ /^(\d{13,16})$/
676 or return gettext('invalid_card'); # . ": ". $self->payinfo;
678 $self->payinfo($payinfo);
680 or return gettext('invalid_card'); # . ": ". $self->payinfo;
681 return gettext('unknown_card_type')
682 if cardtype($self->payinfo) eq "Unknown";
684 } elsif ( $self->payby eq 'CHEK' ) {
686 my $payinfo = $self->payinfo;
687 $payinfo =~ s/[\D\@]//g;
688 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
690 $self->payinfo($payinfo);
692 } elsif ( $self->payby eq 'BILL' ) {
694 $error = $self->ut_textn('payinfo');
695 return "Illegal P.O. number: ". $self->payinfo if $error;
697 } elsif ( $self->payby eq 'COMP' ) {
699 $error = $self->ut_textn('payinfo');
700 return "Illegal comp account issuer: ". $self->payinfo if $error;
702 } elsif ( $self->payby eq 'PREPAY' ) {
704 my $payinfo = $self->payinfo;
705 $payinfo =~ s/\W//g; #anything else would just confuse things
706 $self->payinfo($payinfo);
707 $error = $self->ut_alpha('payinfo');
708 return "Illegal prepayment identifier: ". $self->payinfo if $error;
709 return "Unknown prepayment identifier"
710 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
714 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
715 return "Expriation date required"
716 unless $self->payby =~ /^(BILL|PREPAY|CHEK)$/;
719 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
720 or return "Illegal expiration date: ". $self->paydate;
721 my $y = length($2) == 4 ? $2 : "20$2";
722 $self->paydate("$y-$1-01");
723 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
724 return gettext('expired_card')
725 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
728 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
729 ( ! $conf->exists('require_cardname') || $self->payby ne 'CARD' ) ) {
730 $self->payname( $self->first. " ". $self->getfield('last') );
732 $self->payname =~ /^([\w \,\.\-\']+)$/
733 or return gettext('illegal_name'). " payname: ". $self->payname;
737 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
740 $self->otaker(getotaker);
742 #warn "AFTER: \n". $self->_dump;
749 Returns all packages (see L<FS::cust_pkg>) for this customer.
755 if ( $self->{'_pkgnum'} ) {
756 values %{ $self->{'_pkgnum'}->cache };
758 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
762 =item ncancelled_pkgs
764 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
768 sub ncancelled_pkgs {
770 if ( $self->{'_pkgnum'} ) {
771 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
773 @{ [ # force list context
774 qsearch( 'cust_pkg', {
775 'custnum' => $self->custnum,
778 qsearch( 'cust_pkg', {
779 'custnum' => $self->custnum,
788 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
794 grep { $_->susp } $self->ncancelled_pkgs;
797 =item unflagged_suspended_pkgs
799 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
800 customer (thouse packages without the `manual_flag' set).
804 sub unflagged_suspended_pkgs {
806 return $self->suspended_pkgs
807 unless dbdef->table('cust_pkg')->column('manual_flag');
808 grep { ! $_->manual_flag } $self->suspended_pkgs;
811 =item unsuspended_pkgs
813 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
818 sub unsuspended_pkgs {
820 grep { ! $_->susp } $self->ncancelled_pkgs;
825 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
826 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
827 on success or a list of errors.
833 grep { $_->unsuspend } $self->suspended_pkgs;
838 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
839 Always returns a list: an empty list on success or a list of errors.
845 grep { $_->suspend } $self->unsuspended_pkgs;
850 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
851 Always returns a list: an empty list on success or a list of errors.
857 grep { $_->cancel } $self->ncancelled_pkgs;
862 Returns the agent (see L<FS::agent>) for this customer.
868 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
873 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
874 conjunction with the collect method.
876 Options are passed as name-value pairs.
878 The only currently available option is `time', which bills the customer as if
879 it were that time. It is specified as a UNIX timestamp; see
880 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
881 functions. For example:
885 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
887 If there is an error, returns the error, otherwise returns false.
892 my( $self, %options ) = @_;
893 my $time = $options{'time'} || time;
898 local $SIG{HUP} = 'IGNORE';
899 local $SIG{INT} = 'IGNORE';
900 local $SIG{QUIT} = 'IGNORE';
901 local $SIG{TERM} = 'IGNORE';
902 local $SIG{TSTP} = 'IGNORE';
903 local $SIG{PIPE} = 'IGNORE';
905 my $oldAutoCommit = $FS::UID::AutoCommit;
906 local $FS::UID::AutoCommit = 0;
909 # find the packages which are due for billing, find out how much they are
910 # & generate invoice database.
912 my( $total_setup, $total_recur ) = ( 0, 0 );
913 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
914 my @cust_bill_pkg = ();
916 #my $taxable_charged = 0;##
919 foreach my $cust_pkg (
920 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
923 #NO!! next if $cust_pkg->cancel;
924 next if $cust_pkg->getfield('cancel');
926 #? to avoid use of uninitialized value errors... ?
927 $cust_pkg->setfield('bill', '')
928 unless defined($cust_pkg->bill);
930 my $part_pkg = $cust_pkg->part_pkg;
932 #so we don't modify cust_pkg record unnecessarily
933 my $cust_pkg_mod_flag = 0;
934 my %hash = $cust_pkg->hash;
935 my $old_cust_pkg = new FS::cust_pkg \%hash;
939 unless ( $cust_pkg->setup ) {
940 my $setup_prog = $part_pkg->getfield('setup');
941 $setup_prog =~ /^(.*)$/ or do {
942 $dbh->rollback if $oldAutoCommit;
943 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
949 ##$cpt->permit(); #what is necessary?
950 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
951 #$setup = $cpt->reval($setup_prog);
952 $setup = eval $setup_prog;
953 unless ( defined($setup) ) {
954 $dbh->rollback if $oldAutoCommit;
955 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
956 "(expression $setup_prog): $@";
958 $cust_pkg->setfield('setup',$time);
959 $cust_pkg_mod_flag=1;
965 if ( $part_pkg->getfield('freq') > 0 &&
966 ! $cust_pkg->getfield('susp') &&
967 ( $cust_pkg->getfield('bill') || 0 ) < $time
969 my $recur_prog = $part_pkg->getfield('recur');
970 $recur_prog =~ /^(.*)$/ or do {
971 $dbh->rollback if $oldAutoCommit;
972 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
977 # shared with $recur_prog
978 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
981 ##$cpt->permit(); #what is necessary?
982 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
983 #$recur = $cpt->reval($recur_prog);
984 $recur = eval $recur_prog;
985 unless ( defined($recur) ) {
986 $dbh->rollback if $oldAutoCommit;
987 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
988 "(expression $recur_prog): $@";
990 #change this bit to use Date::Manip? CAREFUL with timezones (see
991 # mailing list archive)
992 my ($sec,$min,$hour,$mday,$mon,$year) =
993 (localtime($sdate) )[0,1,2,3,4,5];
995 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
996 # only for figuring next bill date, nothing else, so, reset $sdate again
998 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1000 $mon += $part_pkg->freq;
1001 until ( $mon < 12 ) { $mon -= 12; $year++; }
1002 $cust_pkg->setfield('bill',
1003 timelocal($sec,$min,$hour,$mday,$mon,$year));
1004 $cust_pkg_mod_flag = 1;
1007 warn "\$setup is undefined" unless defined($setup);
1008 warn "\$recur is undefined" unless defined($recur);
1009 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1011 my $taxable_charged = 0;
1012 if ( $cust_pkg_mod_flag ) {
1013 $error=$cust_pkg->replace($old_cust_pkg);
1014 if ( $error ) { #just in case
1015 $dbh->rollback if $oldAutoCommit;
1016 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1018 $setup = sprintf( "%.2f", $setup );
1019 $recur = sprintf( "%.2f", $recur );
1021 $dbh->rollback if $oldAutoCommit;
1022 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1025 $dbh->rollback if $oldAutoCommit;
1026 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1028 if ( $setup > 0 || $recur > 0 ) {
1029 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1030 'pkgnum' => $cust_pkg->pkgnum,
1034 'edate' => $cust_pkg->bill,
1036 push @cust_bill_pkg, $cust_bill_pkg;
1037 $total_setup += $setup;
1038 $total_recur += $recur;
1039 $taxable_charged += $setup
1040 unless $part_pkg->setuptax =~ /^Y$/i;
1041 $taxable_charged += $recur
1042 unless $part_pkg->recurtax =~ /^Y$/i;
1044 unless ( $self->tax =~ /Y/i
1045 || $self->payby eq 'COMP'
1046 || $taxable_charged == 0 ) {
1048 my $cust_main_county =
1049 qsearchs('cust_main_county',{
1050 'state' => $self->state,
1051 'county' => $self->county,
1052 'country' => $self->country,
1053 'taxclass' => $part_pkg->taxclass,
1055 or qsearchs('cust_main_county',{
1056 'state' => $self->state,
1057 'county' => $self->county,
1058 'country' => $self->country,
1062 $dbh->rollback if $oldAutoCommit;
1064 "fatal: can't find tax rate for state/county/country/taxclass ".
1065 join('/', ( map $self->$_(), qw(state county country) ),
1066 $part_pkg->taxclass ). "\n";
1069 if ( $cust_main_county->exempt_amount ) {
1070 my ($mon,$year) = (localtime($sdate) )[4,5];
1072 my $freq = $part_pkg->freq || 1;
1073 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1074 foreach my $which_month ( 1 .. $freq ) {
1076 'custnum' => $self->custnum,
1077 'taxnum' => $cust_main_county->taxnum,
1078 'year' => 1900+$year,
1081 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1082 until ( $mon < 13 ) { $mon -= 12; $year++; }
1083 my $cust_tax_exempt =
1084 qsearchs('cust_tax_exempt', \%hash)
1085 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1086 my $remaining_exemption = sprintf("%.2f",
1087 $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
1088 if ( $remaining_exemption > 0 ) {
1089 my $addl = $remaining_exemption > $taxable_per_month
1090 ? $taxable_per_month
1091 : $remaining_exemption;
1092 $taxable_charged -= $addl;
1093 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1094 $cust_tax_exempt->hash,
1095 'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1097 $error = $new_cust_tax_exempt->exemptnum
1098 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1099 : $new_cust_tax_exempt->insert;
1101 $dbh->rollback if $oldAutoCommit;
1102 return "fatal: can't update cust_tax_exempt: $error";
1105 } # if $remaining_exemption > 0
1107 } #foreach $which_month
1109 } #if $cust_main_county->exempt_amount
1111 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1112 $tax += $taxable_charged * $cust_main_county->tax / 100
1114 } #unless $self->tax =~ /Y/i
1115 # || $self->payby eq 'COMP'
1116 # || $taxable_charged == 0
1118 } #if $setup > 0 || $recur > 0
1120 } #if $cust_pkg_mod_flag
1122 } #foreach my $cust_pkg
1124 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1125 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1127 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1128 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1132 # unless ( $self->tax =~ /Y/i
1133 # || $self->payby eq 'COMP'
1134 # || $taxable_charged == 0 ) {
1135 # my $cust_main_county = qsearchs('cust_main_county',{
1136 # 'state' => $self->state,
1137 # 'county' => $self->county,
1138 # 'country' => $self->country,
1139 # } ) or die "fatal: can't find tax rate for state/county/country ".
1140 # $self->state. "/". $self->county. "/". $self->country. "\n";
1141 # my $tax = sprintf( "%.2f",
1142 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1145 $tax = sprintf("%.2f", $tax);
1147 $charged = sprintf( "%.2f", $charged+$tax );
1149 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1156 push @cust_bill_pkg, $cust_bill_pkg;
1160 my $cust_bill = new FS::cust_bill ( {
1161 'custnum' => $self->custnum,
1163 'charged' => $charged,
1165 $error = $cust_bill->insert;
1167 $dbh->rollback if $oldAutoCommit;
1168 return "can't create invoice for customer #". $self->custnum. ": $error";
1171 my $invnum = $cust_bill->invnum;
1173 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1175 $cust_bill_pkg->invnum($invnum);
1176 $error = $cust_bill_pkg->insert;
1178 $dbh->rollback if $oldAutoCommit;
1179 return "can't create invoice line item for customer #". $self->custnum.
1184 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1188 =item collect OPTIONS
1190 (Attempt to) collect money for this customer's outstanding invoices (see
1191 L<FS::cust_bill>). Usually used after the bill method.
1193 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1194 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1196 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1197 and the invoice events web interface.
1199 If there is an error, returns the error, otherwise returns false.
1201 Options are passed as name-value pairs.
1203 Currently available options are:
1205 invoice_time - Use this time when deciding when to print invoices and
1206 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>
1207 for conversion functions.
1209 retry_card - Retry cards even when not scheduled by invoice events.
1211 batch_card - This option is deprecated. See the invoice events web interface
1212 to control whether cards are batched or run against a realtime gateway.
1214 report_badcard - This option is deprecated.
1216 force_print - This option is deprecated; see the invoice events web interface.
1221 my( $self, %options ) = @_;
1222 my $invoice_time = $options{'invoice_time'} || time;
1225 local $SIG{HUP} = 'IGNORE';
1226 local $SIG{INT} = 'IGNORE';
1227 local $SIG{QUIT} = 'IGNORE';
1228 local $SIG{TERM} = 'IGNORE';
1229 local $SIG{TSTP} = 'IGNORE';
1230 local $SIG{PIPE} = 'IGNORE';
1232 my $oldAutoCommit = $FS::UID::AutoCommit;
1233 local $FS::UID::AutoCommit = 0;
1236 my $balance = $self->balance;
1237 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1238 unless ( $balance > 0 ) { #redundant?????
1239 $dbh->rollback if $oldAutoCommit; #hmm
1243 if ( exists($options{'retry_card'}) && $options{'retry_card'} ) {
1244 #false laziness w/replace
1245 foreach my $cust_bill_event (
1247 #$_->part_bill_event->plan eq 'realtime-card'
1248 $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
1249 && $_->status eq 'done'
1252 map { $_->cust_bill_event }
1253 grep { $_->cust_bill_event }
1254 $self->open_cust_bill
1256 my $error = $cust_bill_event->retry;
1258 $dbh->rollback if $oldAutoCommit;
1259 return "error scheduling invoice events for retry: $error";
1265 foreach my $cust_bill ( $self->cust_bill ) {
1267 #this has to be before next's
1268 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1272 $balance = sprintf( "%.2f", $balance - $amount );
1274 next unless $cust_bill->owed > 0;
1276 # don't try to charge for the same invoice if it's already in a batch
1277 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1279 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1281 next unless $amount > 0;
1284 foreach my $part_bill_event (
1285 sort { $a->seconds <=> $b->seconds
1286 || $a->weight <=> $b->weight
1287 || $a->eventpart <=> $b->eventpart }
1288 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1289 && ! qsearchs( 'cust_bill_event', {
1290 'invnum' => $cust_bill->invnum,
1291 'eventpart' => $_->eventpart,
1295 qsearch('part_bill_event', { 'payby' => $self->payby,
1296 'disabled' => '', } )
1299 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1301 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1303 my $cust_main = $self; #for callback
1304 my $error = eval $part_bill_event->eventcode;
1307 my $statustext = '';
1311 } elsif ( $error ) {
1313 $statustext = $error;
1318 #add cust_bill_event
1319 my $cust_bill_event = new FS::cust_bill_event {
1320 'invnum' => $cust_bill->invnum,
1321 'eventpart' => $part_bill_event->eventpart,
1322 '_date' => $invoice_time,
1323 'status' => $status,
1324 'statustext' => $statustext,
1326 $error = $cust_bill_event->insert;
1328 #$dbh->rollback if $oldAutoCommit;
1329 #return "error: $error";
1331 # gah, even with transactions.
1332 $dbh->commit if $oldAutoCommit; #well.
1333 my $e = 'WARNING: Event run but database not updated - '.
1334 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1335 ', eventpart '. $part_bill_event->eventpart.
1346 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1353 Returns the total owed for this customer on all invoices
1354 (see L<FS::cust_bill/owed>).
1360 $self->total_owed_date(2145859200); #12/31/2037
1363 =item total_owed_date TIME
1365 Returns the total owed for this customer on all invoices with date earlier than
1366 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1367 see L<Time::Local> and L<Date::Parse> for conversion functions.
1371 sub total_owed_date {
1375 foreach my $cust_bill (
1376 grep { $_->_date <= $time }
1377 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1379 $total_bill += $cust_bill->owed;
1381 sprintf( "%.2f", $total_bill );
1386 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1387 to outstanding invoice balances in chronological order and returns the value
1388 of any remaining unapplied credits available for refund
1389 (see L<FS::cust_refund>).
1396 return 0 unless $self->total_credited;
1398 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1399 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1401 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1402 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1406 foreach my $cust_bill ( @invoices ) {
1409 if ( !defined($credit) || $credit->credited == 0) {
1410 $credit = pop @credits or last;
1413 if ($cust_bill->owed >= $credit->credited) {
1414 $amount=$credit->credited;
1416 $amount=$cust_bill->owed;
1419 my $cust_credit_bill = new FS::cust_credit_bill ( {
1420 'crednum' => $credit->crednum,
1421 'invnum' => $cust_bill->invnum,
1422 'amount' => $amount,
1424 my $error = $cust_credit_bill->insert;
1425 die $error if $error;
1427 redo if ($cust_bill->owed > 0);
1431 return $self->total_credited;
1434 =item apply_payments
1436 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1437 to outstanding invoice balances in chronological order.
1439 #and returns the value of any remaining unapplied payments.
1443 sub apply_payments {
1448 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1449 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1451 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1452 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1456 foreach my $cust_bill ( @invoices ) {
1459 if ( !defined($payment) || $payment->unapplied == 0 ) {
1460 $payment = pop @payments or last;
1463 if ( $cust_bill->owed >= $payment->unapplied ) {
1464 $amount = $payment->unapplied;
1466 $amount = $cust_bill->owed;
1469 my $cust_bill_pay = new FS::cust_bill_pay ( {
1470 'paynum' => $payment->paynum,
1471 'invnum' => $cust_bill->invnum,
1472 'amount' => $amount,
1474 my $error = $cust_bill_pay->insert;
1475 die $error if $error;
1477 redo if ( $cust_bill->owed > 0);
1481 return $self->total_unapplied_payments;
1484 =item total_credited
1486 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1487 customer. See L<FS::cust_credit/credited>.
1491 sub total_credited {
1493 my $total_credit = 0;
1494 foreach my $cust_credit ( qsearch('cust_credit', {
1495 'custnum' => $self->custnum,
1497 $total_credit += $cust_credit->credited;
1499 sprintf( "%.2f", $total_credit );
1502 =item total_unapplied_payments
1504 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1505 See L<FS::cust_pay/unapplied>.
1509 sub total_unapplied_payments {
1511 my $total_unapplied = 0;
1512 foreach my $cust_pay ( qsearch('cust_pay', {
1513 'custnum' => $self->custnum,
1515 $total_unapplied += $cust_pay->unapplied;
1517 sprintf( "%.2f", $total_unapplied );
1522 Returns the balance for this customer (total_owed minus total_credited
1523 minus total_unapplied_payments).
1530 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1534 =item balance_date TIME
1536 Returns the balance for this customer, only considering invoices with date
1537 earlier than TIME (total_owed_date minus total_credited minus
1538 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1539 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1548 $self->total_owed_date($time)
1549 - $self->total_credited
1550 - $self->total_unapplied_payments
1554 =item invoicing_list [ ARRAYREF ]
1556 If an arguement is given, sets these email addresses as invoice recipients
1557 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1558 (except as warnings), so use check_invoicing_list first.
1560 Returns a list of email addresses (with svcnum entries expanded).
1562 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1563 check it without disturbing anything by passing nothing.
1565 This interface may change in the future.
1569 sub invoicing_list {
1570 my( $self, $arrayref ) = @_;
1572 my @cust_main_invoice;
1573 if ( $self->custnum ) {
1574 @cust_main_invoice =
1575 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1577 @cust_main_invoice = ();
1579 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1580 #warn $cust_main_invoice->destnum;
1581 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1582 #warn $cust_main_invoice->destnum;
1583 my $error = $cust_main_invoice->delete;
1584 warn $error if $error;
1587 if ( $self->custnum ) {
1588 @cust_main_invoice =
1589 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1591 @cust_main_invoice = ();
1593 my %seen = map { $_->address => 1 } @cust_main_invoice;
1594 foreach my $address ( @{$arrayref} ) {
1595 next if exists $seen{$address} && $seen{$address};
1596 $seen{$address} = 1;
1597 my $cust_main_invoice = new FS::cust_main_invoice ( {
1598 'custnum' => $self->custnum,
1601 my $error = $cust_main_invoice->insert;
1602 warn $error if $error;
1605 if ( $self->custnum ) {
1607 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1613 =item check_invoicing_list ARRAYREF
1615 Checks these arguements as valid input for the invoicing_list method. If there
1616 is an error, returns the error, otherwise returns false.
1620 sub check_invoicing_list {
1621 my( $self, $arrayref ) = @_;
1622 foreach my $address ( @{$arrayref} ) {
1623 my $cust_main_invoice = new FS::cust_main_invoice ( {
1624 'custnum' => $self->custnum,
1627 my $error = $self->custnum
1628 ? $cust_main_invoice->check
1629 : $cust_main_invoice->checkdest
1631 return $error if $error;
1636 =item set_default_invoicing_list
1638 Sets the invoicing list to all accounts associated with this customer,
1639 overwriting any previous invoicing list.
1643 sub set_default_invoicing_list {
1645 $self->invoicing_list($self->all_emails);
1650 Returns the email addresses of all accounts provisioned for this customer.
1657 foreach my $cust_pkg ( $self->all_pkgs ) {
1658 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1660 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1661 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1663 $list{$_}=1 foreach map { $_->email } @svc_acct;
1668 =item invoicing_list_addpost
1670 Adds postal invoicing to this customer. If this customer is already configured
1671 to receive postal invoices, does nothing.
1675 sub invoicing_list_addpost {
1677 return if grep { $_ eq 'POST' } $self->invoicing_list;
1678 my @invoicing_list = $self->invoicing_list;
1679 push @invoicing_list, 'POST';
1680 $self->invoicing_list(\@invoicing_list);
1683 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1685 Returns an array of customers referred by this customer (referral_custnum set
1686 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1687 customers referred by customers referred by this customer and so on, inclusive.
1688 The default behavior is DEPTH 1 (no recursion).
1692 sub referral_cust_main {
1694 my $depth = @_ ? shift : 1;
1695 my $exclude = @_ ? shift : {};
1698 map { $exclude->{$_->custnum}++; $_; }
1699 grep { ! $exclude->{ $_->custnum } }
1700 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1704 map { $_->referral_cust_main($depth-1, $exclude) }
1711 =item referral_cust_main_ncancelled
1713 Same as referral_cust_main, except only returns customers with uncancelled
1718 sub referral_cust_main_ncancelled {
1720 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
1723 =item referral_cust_pkg [ DEPTH ]
1725 Like referral_cust_main, except returns a flat list of all unsuspended (and
1726 uncancelled) packages for each customer. The number of items in this list may
1727 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
1731 sub referral_cust_pkg {
1733 my $depth = @_ ? shift : 1;
1735 map { $_->unsuspended_pkgs }
1736 grep { $_->unsuspended_pkgs }
1737 $self->referral_cust_main($depth);
1740 =item credit AMOUNT, REASON
1742 Applies a credit to this customer. If there is an error, returns the error,
1743 otherwise returns false.
1748 my( $self, $amount, $reason ) = @_;
1749 my $cust_credit = new FS::cust_credit {
1750 'custnum' => $self->custnum,
1751 'amount' => $amount,
1752 'reason' => $reason,
1754 $cust_credit->insert;
1757 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
1759 Creates a one-time charge for this customer. If there is an error, returns
1760 the error, otherwise returns false.
1765 my ( $self, $amount ) = ( shift, shift );
1766 my $pkg = @_ ? shift : 'One-time charge';
1767 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
1768 my $taxclass = @_ ? shift : '';
1770 local $SIG{HUP} = 'IGNORE';
1771 local $SIG{INT} = 'IGNORE';
1772 local $SIG{QUIT} = 'IGNORE';
1773 local $SIG{TERM} = 'IGNORE';
1774 local $SIG{TSTP} = 'IGNORE';
1775 local $SIG{PIPE} = 'IGNORE';
1777 my $oldAutoCommit = $FS::UID::AutoCommit;
1778 local $FS::UID::AutoCommit = 0;
1781 my $part_pkg = new FS::part_pkg ( {
1783 'comment' => $comment,
1788 'taxclass' => $taxclass,
1791 my $error = $part_pkg->insert;
1793 $dbh->rollback if $oldAutoCommit;
1797 my $pkgpart = $part_pkg->pkgpart;
1798 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
1799 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
1800 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
1801 $error = $type_pkgs->insert;
1803 $dbh->rollback if $oldAutoCommit;
1808 my $cust_pkg = new FS::cust_pkg ( {
1809 'custnum' => $self->custnum,
1810 'pkgpart' => $pkgpart,
1813 $error = $cust_pkg->insert;
1815 $dbh->rollback if $oldAutoCommit;
1819 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1826 Returns all the invoices (see L<FS::cust_bill>) for this customer.
1832 sort { $a->_date <=> $b->_date }
1833 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1836 =item open_cust_bill
1838 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
1843 sub open_cust_bill {
1845 grep { $_->owed > 0 } $self->cust_bill;
1854 =item check_and_rebuild_fuzzyfiles
1858 sub check_and_rebuild_fuzzyfiles {
1859 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1860 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1861 or &rebuild_fuzzyfiles;
1864 =item rebuild_fuzzyfiles
1868 sub rebuild_fuzzyfiles {
1870 use Fcntl qw(:flock);
1872 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1876 open(LASTLOCK,">>$dir/cust_main.last")
1877 or die "can't open $dir/cust_main.last: $!";
1878 flock(LASTLOCK,LOCK_EX)
1879 or die "can't lock $dir/cust_main.last: $!";
1881 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1883 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1884 if defined dbdef->table('cust_main')->column('ship_last');
1886 open (LASTCACHE,">$dir/cust_main.last.tmp")
1887 or die "can't open $dir/cust_main.last.tmp: $!";
1888 print LASTCACHE join("\n", @all_last), "\n";
1889 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1891 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1896 open(COMPANYLOCK,">>$dir/cust_main.company")
1897 or die "can't open $dir/cust_main.company: $!";
1898 flock(COMPANYLOCK,LOCK_EX)
1899 or die "can't lock $dir/cust_main.company: $!";
1901 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1903 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1904 if defined dbdef->table('cust_main')->column('ship_last');
1906 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1907 or die "can't open $dir/cust_main.company.tmp: $!";
1908 print COMPANYCACHE join("\n", @all_company), "\n";
1909 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1911 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1921 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1922 open(LASTCACHE,"<$dir/cust_main.last")
1923 or die "can't open $dir/cust_main.last: $!";
1924 my @array = map { chomp; $_; } <LASTCACHE>;
1934 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1935 open(COMPANYCACHE,"<$dir/cust_main.company")
1936 or die "can't open $dir/cust_main.last: $!";
1937 my @array = map { chomp; $_; } <COMPANYCACHE>;
1942 =item append_fuzzyfiles LASTNAME COMPANY
1946 sub append_fuzzyfiles {
1947 my( $last, $company ) = @_;
1949 &check_and_rebuild_fuzzyfiles;
1951 use Fcntl qw(:flock);
1953 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1957 open(LAST,">>$dir/cust_main.last")
1958 or die "can't open $dir/cust_main.last: $!";
1960 or die "can't lock $dir/cust_main.last: $!";
1962 print LAST "$last\n";
1965 or die "can't unlock $dir/cust_main.last: $!";
1971 open(COMPANY,">>$dir/cust_main.company")
1972 or die "can't open $dir/cust_main.company: $!";
1973 flock(COMPANY,LOCK_EX)
1974 or die "can't lock $dir/cust_main.company: $!";
1976 print COMPANY "$company\n";
1978 flock(COMPANY,LOCK_UN)
1979 or die "can't unlock $dir/cust_main.company: $!";
1993 #warn join('-',keys %$param);
1994 my $fh = $param->{filehandle};
1995 my $agentnum = $param->{agentnum};
1996 my $refnum = $param->{refnum};
1997 my $pkgpart = $param->{pkgpart};
1998 my @fields = @{$param->{fields}};
2000 eval "use Date::Parse;";
2002 eval "use Text::CSV_XS;";
2005 my $csv = new Text::CSV_XS;
2012 local $SIG{HUP} = 'IGNORE';
2013 local $SIG{INT} = 'IGNORE';
2014 local $SIG{QUIT} = 'IGNORE';
2015 local $SIG{TERM} = 'IGNORE';
2016 local $SIG{TSTP} = 'IGNORE';
2017 local $SIG{PIPE} = 'IGNORE';
2019 my $oldAutoCommit = $FS::UID::AutoCommit;
2020 local $FS::UID::AutoCommit = 0;
2023 #while ( $columns = $csv->getline($fh) ) {
2025 while ( defined($line=<$fh>) ) {
2027 $csv->parse($line) or do {
2028 $dbh->rollback if $oldAutoCommit;
2029 return "can't parse: ". $csv->error_input();
2032 my @columns = $csv->fields();
2033 #warn join('-',@columns);
2036 agentnum => $agentnum,
2038 country => 'US', #default
2039 payby => 'BILL', #default
2040 paydate => '12/2037', #default
2042 my $billtime = time;
2043 my %cust_pkg = ( pkgpart => $pkgpart );
2044 foreach my $field ( @fields ) {
2045 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2046 #$cust_pkg{$1} = str2time( shift @$columns );
2047 if ( $1 eq 'setup' ) {
2048 $billtime = str2time(shift @columns);
2050 $cust_pkg{$1} = str2time( shift @columns );
2053 #$cust_main{$field} = shift @$columns;
2054 $cust_main{$field} = shift @columns;
2058 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2059 my $cust_main = new FS::cust_main ( \%cust_main );
2061 tie my %hash, 'Tie::RefHash'; #this part is important
2062 $hash{$cust_pkg} = [] if $pkgpart;
2063 my $error = $cust_main->insert( \%hash );
2066 $dbh->rollback if $oldAutoCommit;
2067 return "can't insert customer for $line: $error";
2070 #false laziness w/bill.cgi
2071 $error = $cust_main->bill( 'time' => $billtime );
2073 $dbh->rollback if $oldAutoCommit;
2074 return "can't bill customer for $line: $error";
2077 $cust_main->apply_payments;
2078 $cust_main->apply_credits;
2080 $error = $cust_main->collect();
2082 $dbh->rollback if $oldAutoCommit;
2083 return "can't collect customer for $line: $error";
2089 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2091 return "Empty file!" unless $imported;
2103 #warn join('-',keys %$param);
2104 my $fh = $param->{filehandle};
2105 my @fields = @{$param->{fields}};
2107 eval "use Date::Parse;";
2109 eval "use Text::CSV_XS;";
2112 my $csv = new Text::CSV_XS;
2119 local $SIG{HUP} = 'IGNORE';
2120 local $SIG{INT} = 'IGNORE';
2121 local $SIG{QUIT} = 'IGNORE';
2122 local $SIG{TERM} = 'IGNORE';
2123 local $SIG{TSTP} = 'IGNORE';
2124 local $SIG{PIPE} = 'IGNORE';
2126 my $oldAutoCommit = $FS::UID::AutoCommit;
2127 local $FS::UID::AutoCommit = 0;
2130 #while ( $columns = $csv->getline($fh) ) {
2132 while ( defined($line=<$fh>) ) {
2134 $csv->parse($line) or do {
2135 $dbh->rollback if $oldAutoCommit;
2136 return "can't parse: ". $csv->error_input();
2139 my @columns = $csv->fields();
2140 #warn join('-',@columns);
2143 foreach my $field ( @fields ) {
2144 $row{$field} = shift @columns;
2147 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2148 unless ( $cust_main ) {
2149 $dbh->rollback if $oldAutoCommit;
2150 return "unknown custnum $row{'custnum'}";
2153 if ( $row{'amount'} > 0 ) {
2154 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2156 $dbh->rollback if $oldAutoCommit;
2160 } elsif ( $row{'amount'} < 0 ) {
2161 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2164 $dbh->rollback if $oldAutoCommit;
2174 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2176 return "Empty file!" unless $imported;
2188 The delete method should possibly take an FS::cust_main object reference
2189 instead of a scalar customer number.
2191 Bill and collect options should probably be passed as references instead of a
2194 There should probably be a configuration file with a list of allowed credit
2197 No multiple currency support (probably a larger project than just this module).
2201 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2202 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2203 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.