4 use vars qw( @ISA $conf $Debug $import );
8 eval "use Time::Local;";
9 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
10 if $] < 5.006 && !defined($Time::Local::VERSION);
11 eval "use Time::Local qw(timelocal timelocal_nocheck);";
15 use Business::CreditCard;
16 use FS::UID qw( getotaker dbh );
17 use FS::Record qw( qsearchs qsearch dbdef );
20 use FS::cust_bill_pkg;
23 use FS::part_referral;
24 use FS::cust_main_county;
26 use FS::cust_main_invoice;
27 use FS::cust_credit_bill;
28 use FS::cust_bill_pay;
29 use FS::prepay_credit;
32 use FS::part_bill_event;
33 use FS::cust_bill_event;
34 use FS::cust_tax_exempt;
36 use FS::Msgcat qw(gettext);
38 @ISA = qw( FS::Record );
45 #ask FS::UID to run this stuff for us later
46 $FS::UID::callback{'FS::cust_main'} = sub {
48 #yes, need it for stuff below (prolly should be cached)
53 my ( $hashref, $cache ) = @_;
54 if ( exists $hashref->{'pkgnum'} ) {
55 # #@{ $self->{'_pkgnum'} } = ();
56 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
57 $self->{'_pkgnum'} = $subcache;
58 #push @{ $self->{'_pkgnum'} },
59 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
65 FS::cust_main - Object methods for cust_main records
71 $record = new FS::cust_main \%hash;
72 $record = new FS::cust_main { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 @cust_pkg = $record->all_pkgs;
84 @cust_pkg = $record->ncancelled_pkgs;
86 @cust_pkg = $record->suspended_pkgs;
88 $error = $record->bill;
89 $error = $record->bill %options;
90 $error = $record->bill 'time' => $time;
92 $error = $record->collect;
93 $error = $record->collect %options;
94 $error = $record->collect 'invoice_time' => $time,
95 'batch_card' => 'yes',
96 'report_badcard' => 'yes',
101 An FS::cust_main object represents a customer. FS::cust_main inherits from
102 FS::Record. The following fields are currently supported:
106 =item custnum - primary key (assigned automatically for new customers)
108 =item agentnum - agent (see L<FS::agent>)
110 =item refnum - Advertising source (see L<FS::part_referral>)
116 =item ss - social security number (optional)
118 =item company - (optional)
122 =item address2 - (optional)
126 =item county - (optional, see L<FS::cust_main_county>)
128 =item state - (see L<FS::cust_main_county>)
132 =item country - (see L<FS::cust_main_county>)
134 =item daytime - phone (optional)
136 =item night - phone (optional)
138 =item fax - phone (optional)
140 =item ship_first - name
142 =item ship_last - name
144 =item ship_company - (optional)
148 =item ship_address2 - (optional)
152 =item ship_county - (optional, see L<FS::cust_main_county>)
154 =item ship_state - (see L<FS::cust_main_county>)
158 =item ship_country - (see L<FS::cust_main_county>)
160 =item ship_daytime - phone (optional)
162 =item ship_night - phone (optional)
164 =item ship_fax - phone (optional)
166 =item payby - `CARD' (credit cards), `CHEK' (electronic check), `LECB' (Phone bill billing), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
168 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
170 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
172 =item payname - name on card or billing name
174 =item tax - tax exempt, empty or `Y'
176 =item otaker - order taker (assigned automatically, see L<FS::UID>)
178 =item comments - comments (optional)
188 Creates a new customer. To add the customer to the database, see L<"insert">.
190 Note that this stores the hash reference, not a distinct copy of the hash it
191 points to. You can ask the object for a copy with the I<hash> method.
195 sub table { 'cust_main'; }
197 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
199 Adds this customer to the database. If there is an error, returns the error,
200 otherwise returns false.
202 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
203 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
204 are inserted atomicly, or the transaction is rolled back. Passing an empty
205 hash reference is equivalent to not supplying this parameter. There should be
206 a better explanation of this, but until then, here's an example:
209 tie %hash, 'Tie::RefHash'; #this part is important
211 $cust_pkg => [ $svc_acct ],
214 $cust_main->insert( \%hash );
216 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
217 be set as the invoicing list (see L<"invoicing_list">). Errors return as
218 expected and rollback the entire transaction; it is not necessary to call
219 check_invoicing_list first. The invoicing_list is set after the records in the
220 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
221 invoicing_list destination to the newly-created svc_acct. Here's an example:
223 $cust_main->insert( {}, [ $email, 'POST' ] );
225 Currently available options are: I<noexport>
227 If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
228 (You can schedule them later with the B<reexport> method.)
234 my $cust_pkgs = @_ ? shift : {};
235 my $invoicing_list = @_ ? shift : '';
238 local $SIG{HUP} = 'IGNORE';
239 local $SIG{INT} = 'IGNORE';
240 local $SIG{QUIT} = 'IGNORE';
241 local $SIG{TERM} = 'IGNORE';
242 local $SIG{TSTP} = 'IGNORE';
243 local $SIG{PIPE} = 'IGNORE';
245 my $oldAutoCommit = $FS::UID::AutoCommit;
246 local $FS::UID::AutoCommit = 0;
251 if ( $self->payby eq 'PREPAY' ) {
252 $self->payby('BILL');
253 my $prepay_credit = qsearchs(
255 { 'identifier' => $self->payinfo },
259 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
260 unless $prepay_credit;
261 $amount = $prepay_credit->amount;
262 $seconds = $prepay_credit->seconds;
263 my $error = $prepay_credit->delete;
265 $dbh->rollback if $oldAutoCommit;
266 return "removing prepay_credit (transaction rolled back): $error";
270 my $error = $self->SUPER::insert;
272 $dbh->rollback if $oldAutoCommit;
273 #return "inserting cust_main record (transaction rolled back): $error";
278 if ( $invoicing_list ) {
279 $error = $self->check_invoicing_list( $invoicing_list );
281 $dbh->rollback if $oldAutoCommit;
282 return "checking invoicing_list (transaction rolled back): $error";
284 $self->invoicing_list( $invoicing_list );
288 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
289 foreach my $cust_pkg ( keys %$cust_pkgs ) {
290 $cust_pkg->custnum( $self->custnum );
291 $error = $cust_pkg->insert;
293 $dbh->rollback if $oldAutoCommit;
294 return "inserting cust_pkg (transaction rolled back): $error";
296 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
297 $svc_something->pkgnum( $cust_pkg->pkgnum );
298 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
299 $svc_something->seconds( $svc_something->seconds + $seconds );
302 $error = $svc_something->insert;
304 $dbh->rollback if $oldAutoCommit;
305 #return "inserting svc_ (transaction rolled back): $error";
312 $dbh->rollback if $oldAutoCommit;
313 return "No svc_acct record to apply pre-paid time";
317 my $cust_credit = new FS::cust_credit {
318 'custnum' => $self->custnum,
321 $error = $cust_credit->insert;
323 $dbh->rollback if $oldAutoCommit;
324 return "inserting credit (transaction rolled back): $error";
328 $error = $self->queue_fuzzyfiles_update;
330 $dbh->rollback if $oldAutoCommit;
331 return "updating fuzzy search cache: $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</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 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
465 && $conf->config('users-allow_comp') ) {
466 return "You are not permitted to create complimentary accounts."
467 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
470 my $oldAutoCommit = $FS::UID::AutoCommit;
471 local $FS::UID::AutoCommit = 0;
474 my $error = $self->SUPER::replace($old);
477 $dbh->rollback if $oldAutoCommit;
481 if ( @param ) { # INVOICING_LIST_ARYREF
482 my $invoicing_list = shift @param;
483 $error = $self->check_invoicing_list( $invoicing_list );
485 $dbh->rollback if $oldAutoCommit;
488 $self->invoicing_list( $invoicing_list );
491 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
492 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
493 # card/check/lec info has changed, want to retry realtime_ invoice events
494 my $error = $self->retry_realtime;
496 $dbh->rollback if $oldAutoCommit;
501 $error = $self->queue_fuzzyfiles_update;
503 $dbh->rollback if $oldAutoCommit;
504 return "updating fuzzy search cache: $error";
507 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
512 =item queue_fuzzyfiles_update
514 Used by insert & replace to update the fuzzy search cache
518 sub queue_fuzzyfiles_update {
521 local $SIG{HUP} = 'IGNORE';
522 local $SIG{INT} = 'IGNORE';
523 local $SIG{QUIT} = 'IGNORE';
524 local $SIG{TERM} = 'IGNORE';
525 local $SIG{TSTP} = 'IGNORE';
526 local $SIG{PIPE} = 'IGNORE';
528 my $oldAutoCommit = $FS::UID::AutoCommit;
529 local $FS::UID::AutoCommit = 0;
532 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
533 my $error = $queue->insert($self->getfield('last'), $self->company);
535 $dbh->rollback if $oldAutoCommit;
536 return "queueing job (transaction rolled back): $error";
539 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
540 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
541 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
543 $dbh->rollback if $oldAutoCommit;
544 return "queueing job (transaction rolled back): $error";
548 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
555 Checks all fields to make sure this is a valid customer record. If there is
556 an error, returns the error, otherwise returns false. Called by the insert
564 #warn "BEFORE: \n". $self->_dump;
567 $self->ut_numbern('custnum')
568 || $self->ut_number('agentnum')
569 || $self->ut_number('refnum')
570 || $self->ut_name('last')
571 || $self->ut_name('first')
572 || $self->ut_textn('company')
573 || $self->ut_text('address1')
574 || $self->ut_textn('address2')
575 || $self->ut_text('city')
576 || $self->ut_textn('county')
577 || $self->ut_textn('state')
578 || $self->ut_country('country')
579 || $self->ut_anything('comments')
580 || $self->ut_numbern('referral_custnum')
582 #barf. need message catalogs. i18n. etc.
583 $error .= "Please select an advertising source."
584 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
585 return $error if $error;
587 return "Unknown agent"
588 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
590 return "Unknown refnum"
591 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
593 return "Unknown referring custnum ". $self->referral_custnum
594 unless ! $self->referral_custnum
595 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
597 if ( $self->ss eq '' ) {
602 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
603 or return "Illegal social security number: ". $self->ss;
604 $self->ss("$1-$2-$3");
608 # bad idea to disable, causes billing to fail because of no tax rates later
609 # unless ( $import ) {
610 unless ( qsearch('cust_main_county', {
611 'country' => $self->country,
614 return "Unknown state/county/country: ".
615 $self->state. "/". $self->county. "/". $self->country
616 unless qsearch('cust_main_county',{
617 'state' => $self->state,
618 'county' => $self->county,
619 'country' => $self->country,
625 $self->ut_phonen('daytime', $self->country)
626 || $self->ut_phonen('night', $self->country)
627 || $self->ut_phonen('fax', $self->country)
628 || $self->ut_zip('zip', $self->country)
630 return $error if $error;
633 last first company address1 address2 city county state zip
634 country daytime night fax
637 if ( defined $self->dbdef_table->column('ship_last') ) {
638 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
640 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
644 $self->ut_name('ship_last')
645 || $self->ut_name('ship_first')
646 || $self->ut_textn('ship_company')
647 || $self->ut_text('ship_address1')
648 || $self->ut_textn('ship_address2')
649 || $self->ut_text('ship_city')
650 || $self->ut_textn('ship_county')
651 || $self->ut_textn('ship_state')
652 || $self->ut_country('ship_country')
654 return $error if $error;
656 #false laziness with above
657 unless ( qsearchs('cust_main_county', {
658 'country' => $self->ship_country,
661 return "Unknown ship_state/ship_county/ship_country: ".
662 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
663 unless qsearchs('cust_main_county',{
664 'state' => $self->ship_state,
665 'county' => $self->ship_county,
666 'country' => $self->ship_country,
672 $self->ut_phonen('ship_daytime', $self->ship_country)
673 || $self->ut_phonen('ship_night', $self->ship_country)
674 || $self->ut_phonen('ship_fax', $self->ship_country)
675 || $self->ut_zip('ship_zip', $self->ship_country)
677 return $error if $error;
679 } else { # ship_ info eq billing info, so don't store dup info in database
680 $self->setfield("ship_$_", '')
681 foreach qw( last first company address1 address2 city county state zip
682 country daytime night fax );
686 $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREPAY)$/
687 or return "Illegal payby: ". $self->payby;
690 if ( $self->payby eq 'CARD' ) {
692 my $payinfo = $self->payinfo;
694 $payinfo =~ /^(\d{13,16})$/
695 or return gettext('invalid_card'); # . ": ". $self->payinfo;
697 $self->payinfo($payinfo);
699 or return gettext('invalid_card'); # . ": ". $self->payinfo;
700 return gettext('unknown_card_type')
701 if cardtype($self->payinfo) eq "Unknown";
703 } elsif ( $self->payby eq 'CHEK' ) {
705 my $payinfo = $self->payinfo;
706 $payinfo =~ s/[^\d\@]//g;
707 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
709 $self->payinfo($payinfo);
711 } elsif ( $self->payby eq 'LECB' ) {
713 my $payinfo = $self->payinfo;
715 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
717 $self->payinfo($payinfo);
719 } elsif ( $self->payby eq 'BILL' ) {
721 $error = $self->ut_textn('payinfo');
722 return "Illegal P.O. number: ". $self->payinfo if $error;
724 } elsif ( $self->payby eq 'COMP' ) {
726 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
727 return "You are not permitted to create complimentary accounts."
728 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
731 $error = $self->ut_textn('payinfo');
732 return "Illegal comp account issuer: ". $self->payinfo if $error;
734 } elsif ( $self->payby eq 'PREPAY' ) {
736 my $payinfo = $self->payinfo;
737 $payinfo =~ s/\W//g; #anything else would just confuse things
738 $self->payinfo($payinfo);
739 $error = $self->ut_alpha('payinfo');
740 return "Illegal prepayment identifier: ". $self->payinfo if $error;
741 return "Unknown prepayment identifier"
742 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
746 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
747 return "Expriation date required"
748 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
751 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
752 or return "Illegal expiration date: ". $self->paydate;
753 my $y = length($2) == 4 ? $2 : "20$2";
754 $self->paydate("$y-$1-01");
755 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
756 return gettext('expired_card')
757 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
760 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
761 ( ! $conf->exists('require_cardname') || $self->payby ne 'CARD' ) ) {
762 $self->payname( $self->first. " ". $self->getfield('last') );
764 $self->payname =~ /^([\w \,\.\-\']+)$/
765 or return gettext('illegal_name'). " payname: ". $self->payname;
769 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
772 $self->otaker(getotaker);
774 #warn "AFTER: \n". $self->_dump;
781 Returns all packages (see L<FS::cust_pkg>) for this customer.
787 if ( $self->{'_pkgnum'} ) {
788 values %{ $self->{'_pkgnum'}->cache };
790 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
794 =item ncancelled_pkgs
796 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
800 sub ncancelled_pkgs {
802 if ( $self->{'_pkgnum'} ) {
803 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
805 @{ [ # force list context
806 qsearch( 'cust_pkg', {
807 'custnum' => $self->custnum,
810 qsearch( 'cust_pkg', {
811 'custnum' => $self->custnum,
820 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
826 grep { $_->susp } $self->ncancelled_pkgs;
829 =item unflagged_suspended_pkgs
831 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
832 customer (thouse packages without the `manual_flag' set).
836 sub unflagged_suspended_pkgs {
838 return $self->suspended_pkgs
839 unless dbdef->table('cust_pkg')->column('manual_flag');
840 grep { ! $_->manual_flag } $self->suspended_pkgs;
843 =item unsuspended_pkgs
845 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
850 sub unsuspended_pkgs {
852 grep { ! $_->susp } $self->ncancelled_pkgs;
857 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
858 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
859 on success or a list of errors.
865 grep { $_->unsuspend } $self->suspended_pkgs;
870 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
871 Always returns a list: an empty list on success or a list of errors.
877 grep { $_->suspend } $self->unsuspended_pkgs;
880 =item cancel [ OPTION => VALUE ... ]
882 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
884 Available options are: I<quiet>
886 I<quiet> can be set true to supress email cancellation notices.
888 Always returns a list: an empty list on success or a list of errors.
894 grep { $_->cancel(@_) } $self->ncancelled_pkgs;
899 Returns the agent (see L<FS::agent>) for this customer.
905 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
910 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
911 conjunction with the collect method.
913 Options are passed as name-value pairs.
915 The only currently available option is `time', which bills the customer as if
916 it were that time. It is specified as a UNIX timestamp; see
917 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
918 functions. For example:
922 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
924 If there is an error, returns the error, otherwise returns false.
929 my( $self, %options ) = @_;
930 my $time = $options{'time'} || time;
935 local $SIG{HUP} = 'IGNORE';
936 local $SIG{INT} = 'IGNORE';
937 local $SIG{QUIT} = 'IGNORE';
938 local $SIG{TERM} = 'IGNORE';
939 local $SIG{TSTP} = 'IGNORE';
940 local $SIG{PIPE} = 'IGNORE';
942 my $oldAutoCommit = $FS::UID::AutoCommit;
943 local $FS::UID::AutoCommit = 0;
946 # find the packages which are due for billing, find out how much they are
947 # & generate invoice database.
949 my( $total_setup, $total_recur ) = ( 0, 0 );
950 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
951 my @cust_bill_pkg = ();
953 #my $taxable_charged = 0;##
956 foreach my $cust_pkg (
957 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
960 #NO!! next if $cust_pkg->cancel;
961 next if $cust_pkg->getfield('cancel');
963 #? to avoid use of uninitialized value errors... ?
964 $cust_pkg->setfield('bill', '')
965 unless defined($cust_pkg->bill);
967 my $part_pkg = $cust_pkg->part_pkg;
969 #so we don't modify cust_pkg record unnecessarily
970 my $cust_pkg_mod_flag = 0;
971 my %hash = $cust_pkg->hash;
972 my $old_cust_pkg = new FS::cust_pkg \%hash;
976 unless ( $cust_pkg->setup ) {
977 my $setup_prog = $part_pkg->getfield('setup');
978 $setup_prog =~ /^(.*)$/ or do {
979 $dbh->rollback if $oldAutoCommit;
980 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
984 $setup_prog = '0' if $setup_prog =~ /^\s*$/;
987 ##$cpt->permit(); #what is necessary?
988 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
989 #$setup = $cpt->reval($setup_prog);
990 $setup = eval $setup_prog;
991 unless ( defined($setup) ) {
992 $dbh->rollback if $oldAutoCommit;
993 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
994 "(expression $setup_prog): $@";
996 $cust_pkg->setfield('setup',$time);
997 $cust_pkg_mod_flag=1;
1003 if ( $part_pkg->getfield('freq') > 0 &&
1004 ! $cust_pkg->getfield('susp') &&
1005 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1007 my $recur_prog = $part_pkg->getfield('recur');
1008 $recur_prog =~ /^(.*)$/ or do {
1009 $dbh->rollback if $oldAutoCommit;
1010 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1014 $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1016 # shared with $recur_prog
1017 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1019 #my $cpt = new Safe;
1020 ##$cpt->permit(); #what is necessary?
1021 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1022 #$recur = $cpt->reval($recur_prog);
1023 $recur = eval $recur_prog;
1024 unless ( defined($recur) ) {
1025 $dbh->rollback if $oldAutoCommit;
1026 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1027 "(expression $recur_prog): $@";
1029 #change this bit to use Date::Manip? CAREFUL with timezones (see
1030 # mailing list archive)
1031 my ($sec,$min,$hour,$mday,$mon,$year) =
1032 (localtime($sdate) )[0,1,2,3,4,5];
1034 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1035 # only for figuring next bill date, nothing else, so, reset $sdate again
1037 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1038 $cust_pkg->last_bill($sdate)
1039 if $cust_pkg->dbdef_table->column('last_bill');
1041 $mon += $part_pkg->freq;
1042 until ( $mon < 12 ) { $mon -= 12; $year++; }
1043 $cust_pkg->setfield('bill',
1044 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1045 $cust_pkg_mod_flag = 1;
1048 warn "\$setup is undefined" unless defined($setup);
1049 warn "\$recur is undefined" unless defined($recur);
1050 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1052 my $taxable_charged = 0;
1053 if ( $cust_pkg_mod_flag ) {
1054 $error=$cust_pkg->replace($old_cust_pkg);
1055 if ( $error ) { #just in case
1056 $dbh->rollback if $oldAutoCommit;
1057 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1059 $setup = sprintf( "%.2f", $setup );
1060 $recur = sprintf( "%.2f", $recur );
1062 $dbh->rollback if $oldAutoCommit;
1063 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1066 $dbh->rollback if $oldAutoCommit;
1067 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1069 if ( $setup > 0 || $recur > 0 ) {
1070 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1071 'pkgnum' => $cust_pkg->pkgnum,
1075 'edate' => $cust_pkg->bill,
1077 push @cust_bill_pkg, $cust_bill_pkg;
1078 $total_setup += $setup;
1079 $total_recur += $recur;
1080 $taxable_charged += $setup
1081 unless $part_pkg->setuptax =~ /^Y$/i;
1082 $taxable_charged += $recur
1083 unless $part_pkg->recurtax =~ /^Y$/i;
1085 unless ( $self->tax =~ /Y/i
1086 || $self->payby eq 'COMP'
1087 || $taxable_charged == 0 ) {
1089 my $cust_main_county = qsearchs('cust_main_county',{
1090 'state' => $self->state,
1091 'county' => $self->county,
1092 'country' => $self->country,
1093 'taxclass' => $part_pkg->taxclass,
1095 $cust_main_county ||= qsearchs('cust_main_county',{
1096 'state' => $self->state,
1097 'county' => $self->county,
1098 'country' => $self->country,
1101 unless ( $cust_main_county ) {
1102 $dbh->rollback if $oldAutoCommit;
1104 "fatal: can't find tax rate for state/county/country/taxclass ".
1105 join('/', ( map $self->$_(), qw(state county country) ),
1106 $part_pkg->taxclass ). "\n";
1109 if ( $cust_main_county->exempt_amount ) {
1110 my ($mon,$year) = (localtime($sdate) )[4,5];
1112 my $freq = $part_pkg->freq || 1;
1113 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1114 foreach my $which_month ( 1 .. $freq ) {
1116 'custnum' => $self->custnum,
1117 'taxnum' => $cust_main_county->taxnum,
1118 'year' => 1900+$year,
1121 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1122 until ( $mon < 13 ) { $mon -= 12; $year++; }
1123 my $cust_tax_exempt =
1124 qsearchs('cust_tax_exempt', \%hash)
1125 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1126 my $remaining_exemption = sprintf("%.2f",
1127 $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
1128 if ( $remaining_exemption > 0 ) {
1129 my $addl = $remaining_exemption > $taxable_per_month
1130 ? $taxable_per_month
1131 : $remaining_exemption;
1132 $taxable_charged -= $addl;
1133 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1134 $cust_tax_exempt->hash,
1135 'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1137 $error = $new_cust_tax_exempt->exemptnum
1138 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1139 : $new_cust_tax_exempt->insert;
1141 $dbh->rollback if $oldAutoCommit;
1142 return "fatal: can't update cust_tax_exempt: $error";
1145 } # if $remaining_exemption > 0
1147 } #foreach $which_month
1149 } #if $cust_main_county->exempt_amount
1151 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1152 $tax += $taxable_charged * $cust_main_county->tax / 100
1154 } #unless $self->tax =~ /Y/i
1155 # || $self->payby eq 'COMP'
1156 # || $taxable_charged == 0
1158 } #if $setup > 0 || $recur > 0
1160 } #if $cust_pkg_mod_flag
1162 } #foreach my $cust_pkg
1164 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1165 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1167 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1168 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1172 # unless ( $self->tax =~ /Y/i
1173 # || $self->payby eq 'COMP'
1174 # || $taxable_charged == 0 ) {
1175 # my $cust_main_county = qsearchs('cust_main_county',{
1176 # 'state' => $self->state,
1177 # 'county' => $self->county,
1178 # 'country' => $self->country,
1179 # } ) or die "fatal: can't find tax rate for state/county/country ".
1180 # $self->state. "/". $self->county. "/". $self->country. "\n";
1181 # my $tax = sprintf( "%.2f",
1182 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1185 $tax = sprintf("%.2f", $tax);
1187 $charged = sprintf( "%.2f", $charged+$tax );
1189 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1196 push @cust_bill_pkg, $cust_bill_pkg;
1200 my $cust_bill = new FS::cust_bill ( {
1201 'custnum' => $self->custnum,
1203 'charged' => $charged,
1205 $error = $cust_bill->insert;
1207 $dbh->rollback if $oldAutoCommit;
1208 return "can't create invoice for customer #". $self->custnum. ": $error";
1211 my $invnum = $cust_bill->invnum;
1213 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1215 $cust_bill_pkg->invnum($invnum);
1216 $error = $cust_bill_pkg->insert;
1218 $dbh->rollback if $oldAutoCommit;
1219 return "can't create invoice line item for customer #". $self->custnum.
1224 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1230 document me. Re-schedules all exports by calling the B<reexport> method
1231 of all associated packages (see L<FS::cust_pkg>). If there is an error,
1232 returns the error; otherwise returns false.
1239 local $SIG{HUP} = 'IGNORE';
1240 local $SIG{INT} = 'IGNORE';
1241 local $SIG{QUIT} = 'IGNORE';
1242 local $SIG{TERM} = 'IGNORE';
1243 local $SIG{TSTP} = 'IGNORE';
1244 local $SIG{PIPE} = 'IGNORE';
1246 my $oldAutoCommit = $FS::UID::AutoCommit;
1247 local $FS::UID::AutoCommit = 0;
1250 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
1251 my $error = $cust_pkg->reexport;
1253 $dbh->rollback if $oldAutoCommit;
1258 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1263 =item collect OPTIONS
1265 (Attempt to) collect money for this customer's outstanding invoices (see
1266 L<FS::cust_bill>). Usually used after the bill method.
1268 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1269 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1271 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1272 and the invoice events web interface.
1274 If there is an error, returns the error, otherwise returns false.
1276 Options are passed as name-value pairs.
1278 Currently available options are:
1280 invoice_time - Use this time when deciding when to print invoices and
1281 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>
1282 for conversion functions.
1284 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1287 retry_card - Deprecated alias for 'retry'
1289 batch_card - This option is deprecated. See the invoice events web interface
1290 to control whether cards are batched or run against a realtime gateway.
1292 report_badcard - This option is deprecated.
1294 force_print - This option is deprecated; see the invoice events web interface.
1296 quiet - set true to surpress email card/ACH decline notices.
1301 my( $self, %options ) = @_;
1302 my $invoice_time = $options{'invoice_time'} || time;
1305 local $SIG{HUP} = 'IGNORE';
1306 local $SIG{INT} = 'IGNORE';
1307 local $SIG{QUIT} = 'IGNORE';
1308 local $SIG{TERM} = 'IGNORE';
1309 local $SIG{TSTP} = 'IGNORE';
1310 local $SIG{PIPE} = 'IGNORE';
1312 my $oldAutoCommit = $FS::UID::AutoCommit;
1313 local $FS::UID::AutoCommit = 0;
1316 my $balance = $self->balance;
1317 warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1318 unless ( $balance > 0 ) { #redundant?????
1319 $dbh->rollback if $oldAutoCommit; #hmm
1323 if ( exists($options{'retry_card'}) ) {
1324 carp 'retry_card option passed to collect is deprecated; use retry';
1325 $options{'retry'} ||= $options{'retry_card'};
1327 if ( exists($options{'retry'}) && $options{'retry'} ) {
1328 my $error = $self->retry_realtime;
1330 $dbh->rollback if $oldAutoCommit;
1335 foreach my $cust_bill ( $self->cust_bill ) {
1337 #this has to be before next's
1338 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1342 $balance = sprintf( "%.2f", $balance - $amount );
1344 next unless $cust_bill->owed > 0;
1346 # don't try to charge for the same invoice if it's already in a batch
1347 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1349 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1351 next unless $amount > 0;
1354 foreach my $part_bill_event (
1355 sort { $a->seconds <=> $b->seconds
1356 || $a->weight <=> $b->weight
1357 || $a->eventpart <=> $b->eventpart }
1358 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1359 && ! qsearchs( 'cust_bill_event', {
1360 'invnum' => $cust_bill->invnum,
1361 'eventpart' => $_->eventpart,
1365 qsearch('part_bill_event', { 'payby' => $self->payby,
1366 'disabled' => '', } )
1369 last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1371 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1373 my $cust_main = $self; #for callback
1377 #supress "used only once" warning
1378 $FS::cust_bill::realtime_bop_decline_quiet += 0;
1379 local $FS::cust_bill::realtime_bop_decline_quiet = 1
1380 if $options{'quiet'};
1381 $error = eval $part_bill_event->eventcode;
1385 my $statustext = '';
1389 } elsif ( $error ) {
1391 $statustext = $error;
1396 #add cust_bill_event
1397 my $cust_bill_event = new FS::cust_bill_event {
1398 'invnum' => $cust_bill->invnum,
1399 'eventpart' => $part_bill_event->eventpart,
1400 #'_date' => $invoice_time,
1402 'status' => $status,
1403 'statustext' => $statustext,
1405 $error = $cust_bill_event->insert;
1407 #$dbh->rollback if $oldAutoCommit;
1408 #return "error: $error";
1410 # gah, even with transactions.
1411 $dbh->commit if $oldAutoCommit; #well.
1412 my $e = 'WARNING: Event run but database not updated - '.
1413 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1414 ', eventpart '. $part_bill_event->eventpart.
1425 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1430 =item retry_realtime
1432 Schedules realtime credit card / electronic check / LEC billing events for
1433 for retry. Useful if card information has changed or manual retry is desired.
1434 The 'collect' method must be called to actually retry the transaction.
1436 Implementation details: For each of this customer's open invoices, changes
1437 the status of the first "done" (with statustext error) realtime processing
1442 sub retry_realtime {
1445 local $SIG{HUP} = 'IGNORE';
1446 local $SIG{INT} = 'IGNORE';
1447 local $SIG{QUIT} = 'IGNORE';
1448 local $SIG{TERM} = 'IGNORE';
1449 local $SIG{TSTP} = 'IGNORE';
1450 local $SIG{PIPE} = 'IGNORE';
1452 my $oldAutoCommit = $FS::UID::AutoCommit;
1453 local $FS::UID::AutoCommit = 0;
1456 foreach my $cust_bill (
1457 grep { $_->cust_bill_event }
1458 $self->open_cust_bill
1460 my @cust_bill_event =
1461 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1463 #$_->part_bill_event->plan eq 'realtime-card'
1464 $_->part_bill_event->eventcode =~
1465 /\$cust_bill\->realtime_(card|ach|lec)/
1466 && $_->status eq 'done'
1469 $cust_bill->cust_bill_event;
1470 next unless @cust_bill_event;
1471 my $error = $cust_bill_event[0]->retry;
1473 $dbh->rollback if $oldAutoCommit;
1474 return "error scheduling invoice event for retry: $error";
1479 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1486 Returns the total owed for this customer on all invoices
1487 (see L<FS::cust_bill/owed>).
1493 $self->total_owed_date(2145859200); #12/31/2037
1496 =item total_owed_date TIME
1498 Returns the total owed for this customer on all invoices with date earlier than
1499 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1500 see L<Time::Local> and L<Date::Parse> for conversion functions.
1504 sub total_owed_date {
1508 foreach my $cust_bill (
1509 grep { $_->_date <= $time }
1510 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1512 $total_bill += $cust_bill->owed;
1514 sprintf( "%.2f", $total_bill );
1519 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1520 to outstanding invoice balances in chronological order and returns the value
1521 of any remaining unapplied credits available for refund
1522 (see L<FS::cust_refund>).
1529 return 0 unless $self->total_credited;
1531 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1532 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1534 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1535 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1539 foreach my $cust_bill ( @invoices ) {
1542 if ( !defined($credit) || $credit->credited == 0) {
1543 $credit = pop @credits or last;
1546 if ($cust_bill->owed >= $credit->credited) {
1547 $amount=$credit->credited;
1549 $amount=$cust_bill->owed;
1552 my $cust_credit_bill = new FS::cust_credit_bill ( {
1553 'crednum' => $credit->crednum,
1554 'invnum' => $cust_bill->invnum,
1555 'amount' => $amount,
1557 my $error = $cust_credit_bill->insert;
1558 die $error if $error;
1560 redo if ($cust_bill->owed > 0);
1564 return $self->total_credited;
1567 =item apply_payments
1569 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1570 to outstanding invoice balances in chronological order.
1572 #and returns the value of any remaining unapplied payments.
1576 sub apply_payments {
1581 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1582 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1584 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1585 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1589 foreach my $cust_bill ( @invoices ) {
1592 if ( !defined($payment) || $payment->unapplied == 0 ) {
1593 $payment = pop @payments or last;
1596 if ( $cust_bill->owed >= $payment->unapplied ) {
1597 $amount = $payment->unapplied;
1599 $amount = $cust_bill->owed;
1602 my $cust_bill_pay = new FS::cust_bill_pay ( {
1603 'paynum' => $payment->paynum,
1604 'invnum' => $cust_bill->invnum,
1605 'amount' => $amount,
1607 my $error = $cust_bill_pay->insert;
1608 die $error if $error;
1610 redo if ( $cust_bill->owed > 0);
1614 return $self->total_unapplied_payments;
1617 =item total_credited
1619 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1620 customer. See L<FS::cust_credit/credited>.
1624 sub total_credited {
1626 my $total_credit = 0;
1627 foreach my $cust_credit ( qsearch('cust_credit', {
1628 'custnum' => $self->custnum,
1630 $total_credit += $cust_credit->credited;
1632 sprintf( "%.2f", $total_credit );
1635 =item total_unapplied_payments
1637 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1638 See L<FS::cust_pay/unapplied>.
1642 sub total_unapplied_payments {
1644 my $total_unapplied = 0;
1645 foreach my $cust_pay ( qsearch('cust_pay', {
1646 'custnum' => $self->custnum,
1648 $total_unapplied += $cust_pay->unapplied;
1650 sprintf( "%.2f", $total_unapplied );
1655 Returns the balance for this customer (total_owed minus total_credited
1656 minus total_unapplied_payments).
1663 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1667 =item balance_date TIME
1669 Returns the balance for this customer, only considering invoices with date
1670 earlier than TIME (total_owed_date minus total_credited minus
1671 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1672 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1681 $self->total_owed_date($time)
1682 - $self->total_credited
1683 - $self->total_unapplied_payments
1687 =item invoicing_list [ ARRAYREF ]
1689 If an arguement is given, sets these email addresses as invoice recipients
1690 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1691 (except as warnings), so use check_invoicing_list first.
1693 Returns a list of email addresses (with svcnum entries expanded).
1695 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1696 check it without disturbing anything by passing nothing.
1698 This interface may change in the future.
1702 sub invoicing_list {
1703 my( $self, $arrayref ) = @_;
1705 my @cust_main_invoice;
1706 if ( $self->custnum ) {
1707 @cust_main_invoice =
1708 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1710 @cust_main_invoice = ();
1712 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1713 #warn $cust_main_invoice->destnum;
1714 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1715 #warn $cust_main_invoice->destnum;
1716 my $error = $cust_main_invoice->delete;
1717 warn $error if $error;
1720 if ( $self->custnum ) {
1721 @cust_main_invoice =
1722 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1724 @cust_main_invoice = ();
1726 my %seen = map { $_->address => 1 } @cust_main_invoice;
1727 foreach my $address ( @{$arrayref} ) {
1728 next if exists $seen{$address} && $seen{$address};
1729 $seen{$address} = 1;
1730 my $cust_main_invoice = new FS::cust_main_invoice ( {
1731 'custnum' => $self->custnum,
1734 my $error = $cust_main_invoice->insert;
1735 warn $error if $error;
1738 if ( $self->custnum ) {
1740 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1746 =item check_invoicing_list ARRAYREF
1748 Checks these arguements as valid input for the invoicing_list method. If there
1749 is an error, returns the error, otherwise returns false.
1753 sub check_invoicing_list {
1754 my( $self, $arrayref ) = @_;
1755 foreach my $address ( @{$arrayref} ) {
1756 my $cust_main_invoice = new FS::cust_main_invoice ( {
1757 'custnum' => $self->custnum,
1760 my $error = $self->custnum
1761 ? $cust_main_invoice->check
1762 : $cust_main_invoice->checkdest
1764 return $error if $error;
1769 =item set_default_invoicing_list
1771 Sets the invoicing list to all accounts associated with this customer,
1772 overwriting any previous invoicing list.
1776 sub set_default_invoicing_list {
1778 $self->invoicing_list($self->all_emails);
1783 Returns the email addresses of all accounts provisioned for this customer.
1790 foreach my $cust_pkg ( $self->all_pkgs ) {
1791 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1793 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1794 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1796 $list{$_}=1 foreach map { $_->email } @svc_acct;
1801 =item invoicing_list_addpost
1803 Adds postal invoicing to this customer. If this customer is already configured
1804 to receive postal invoices, does nothing.
1808 sub invoicing_list_addpost {
1810 return if grep { $_ eq 'POST' } $self->invoicing_list;
1811 my @invoicing_list = $self->invoicing_list;
1812 push @invoicing_list, 'POST';
1813 $self->invoicing_list(\@invoicing_list);
1816 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1818 Returns an array of customers referred by this customer (referral_custnum set
1819 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1820 customers referred by customers referred by this customer and so on, inclusive.
1821 The default behavior is DEPTH 1 (no recursion).
1825 sub referral_cust_main {
1827 my $depth = @_ ? shift : 1;
1828 my $exclude = @_ ? shift : {};
1831 map { $exclude->{$_->custnum}++; $_; }
1832 grep { ! $exclude->{ $_->custnum } }
1833 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1837 map { $_->referral_cust_main($depth-1, $exclude) }
1844 =item referral_cust_main_ncancelled
1846 Same as referral_cust_main, except only returns customers with uncancelled
1851 sub referral_cust_main_ncancelled {
1853 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
1856 =item referral_cust_pkg [ DEPTH ]
1858 Like referral_cust_main, except returns a flat list of all unsuspended (and
1859 uncancelled) packages for each customer. The number of items in this list may
1860 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
1864 sub referral_cust_pkg {
1866 my $depth = @_ ? shift : 1;
1868 map { $_->unsuspended_pkgs }
1869 grep { $_->unsuspended_pkgs }
1870 $self->referral_cust_main($depth);
1873 =item credit AMOUNT, REASON
1875 Applies a credit to this customer. If there is an error, returns the error,
1876 otherwise returns false.
1881 my( $self, $amount, $reason ) = @_;
1882 my $cust_credit = new FS::cust_credit {
1883 'custnum' => $self->custnum,
1884 'amount' => $amount,
1885 'reason' => $reason,
1887 $cust_credit->insert;
1890 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
1892 Creates a one-time charge for this customer. If there is an error, returns
1893 the error, otherwise returns false.
1898 my ( $self, $amount ) = ( shift, shift );
1899 my $pkg = @_ ? shift : 'One-time charge';
1900 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
1901 my $taxclass = @_ ? shift : '';
1903 local $SIG{HUP} = 'IGNORE';
1904 local $SIG{INT} = 'IGNORE';
1905 local $SIG{QUIT} = 'IGNORE';
1906 local $SIG{TERM} = 'IGNORE';
1907 local $SIG{TSTP} = 'IGNORE';
1908 local $SIG{PIPE} = 'IGNORE';
1910 my $oldAutoCommit = $FS::UID::AutoCommit;
1911 local $FS::UID::AutoCommit = 0;
1914 my $part_pkg = new FS::part_pkg ( {
1916 'comment' => $comment,
1921 'taxclass' => $taxclass,
1924 my $error = $part_pkg->insert;
1926 $dbh->rollback if $oldAutoCommit;
1930 my $pkgpart = $part_pkg->pkgpart;
1931 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
1932 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
1933 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
1934 $error = $type_pkgs->insert;
1936 $dbh->rollback if $oldAutoCommit;
1941 my $cust_pkg = new FS::cust_pkg ( {
1942 'custnum' => $self->custnum,
1943 'pkgpart' => $pkgpart,
1946 $error = $cust_pkg->insert;
1948 $dbh->rollback if $oldAutoCommit;
1952 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1959 Returns all the invoices (see L<FS::cust_bill>) for this customer.
1965 sort { $a->_date <=> $b->_date }
1966 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1969 =item open_cust_bill
1971 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
1976 sub open_cust_bill {
1978 grep { $_->owed > 0 } $self->cust_bill;
1987 =item check_and_rebuild_fuzzyfiles
1991 sub check_and_rebuild_fuzzyfiles {
1992 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1993 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1994 or &rebuild_fuzzyfiles;
1997 =item rebuild_fuzzyfiles
2001 sub rebuild_fuzzyfiles {
2003 use Fcntl qw(:flock);
2005 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2009 open(LASTLOCK,">>$dir/cust_main.last")
2010 or die "can't open $dir/cust_main.last: $!";
2011 flock(LASTLOCK,LOCK_EX)
2012 or die "can't lock $dir/cust_main.last: $!";
2014 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2016 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2017 if defined dbdef->table('cust_main')->column('ship_last');
2019 open (LASTCACHE,">$dir/cust_main.last.tmp")
2020 or die "can't open $dir/cust_main.last.tmp: $!";
2021 print LASTCACHE join("\n", @all_last), "\n";
2022 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2024 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
2029 open(COMPANYLOCK,">>$dir/cust_main.company")
2030 or die "can't open $dir/cust_main.company: $!";
2031 flock(COMPANYLOCK,LOCK_EX)
2032 or die "can't lock $dir/cust_main.company: $!";
2034 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
2036 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
2037 if defined dbdef->table('cust_main')->column('ship_last');
2039 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
2040 or die "can't open $dir/cust_main.company.tmp: $!";
2041 print COMPANYCACHE join("\n", @all_company), "\n";
2042 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
2044 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
2054 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2055 open(LASTCACHE,"<$dir/cust_main.last")
2056 or die "can't open $dir/cust_main.last: $!";
2057 my @array = map { chomp; $_; } <LASTCACHE>;
2067 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2068 open(COMPANYCACHE,"<$dir/cust_main.company")
2069 or die "can't open $dir/cust_main.last: $!";
2070 my @array = map { chomp; $_; } <COMPANYCACHE>;
2075 =item append_fuzzyfiles LASTNAME COMPANY
2079 sub append_fuzzyfiles {
2080 my( $last, $company ) = @_;
2082 &check_and_rebuild_fuzzyfiles;
2084 use Fcntl qw(:flock);
2086 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2090 open(LAST,">>$dir/cust_main.last")
2091 or die "can't open $dir/cust_main.last: $!";
2093 or die "can't lock $dir/cust_main.last: $!";
2095 print LAST "$last\n";
2098 or die "can't unlock $dir/cust_main.last: $!";
2104 open(COMPANY,">>$dir/cust_main.company")
2105 or die "can't open $dir/cust_main.company: $!";
2106 flock(COMPANY,LOCK_EX)
2107 or die "can't lock $dir/cust_main.company: $!";
2109 print COMPANY "$company\n";
2111 flock(COMPANY,LOCK_UN)
2112 or die "can't unlock $dir/cust_main.company: $!";
2126 #warn join('-',keys %$param);
2127 my $fh = $param->{filehandle};
2128 my $agentnum = $param->{agentnum};
2129 my $refnum = $param->{refnum};
2130 my $pkgpart = $param->{pkgpart};
2131 my @fields = @{$param->{fields}};
2133 eval "use Date::Parse;";
2135 eval "use Text::CSV_XS;";
2138 my $csv = new Text::CSV_XS;
2145 local $SIG{HUP} = 'IGNORE';
2146 local $SIG{INT} = 'IGNORE';
2147 local $SIG{QUIT} = 'IGNORE';
2148 local $SIG{TERM} = 'IGNORE';
2149 local $SIG{TSTP} = 'IGNORE';
2150 local $SIG{PIPE} = 'IGNORE';
2152 my $oldAutoCommit = $FS::UID::AutoCommit;
2153 local $FS::UID::AutoCommit = 0;
2156 #while ( $columns = $csv->getline($fh) ) {
2158 while ( defined($line=<$fh>) ) {
2160 $csv->parse($line) or do {
2161 $dbh->rollback if $oldAutoCommit;
2162 return "can't parse: ". $csv->error_input();
2165 my @columns = $csv->fields();
2166 #warn join('-',@columns);
2169 agentnum => $agentnum,
2171 country => 'US', #default
2172 payby => 'BILL', #default
2173 paydate => '12/2037', #default
2175 my $billtime = time;
2176 my %cust_pkg = ( pkgpart => $pkgpart );
2177 foreach my $field ( @fields ) {
2178 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2179 #$cust_pkg{$1} = str2time( shift @$columns );
2180 if ( $1 eq 'setup' ) {
2181 $billtime = str2time(shift @columns);
2183 $cust_pkg{$1} = str2time( shift @columns );
2186 #$cust_main{$field} = shift @$columns;
2187 $cust_main{$field} = shift @columns;
2191 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2192 my $cust_main = new FS::cust_main ( \%cust_main );
2194 tie my %hash, 'Tie::RefHash'; #this part is important
2195 $hash{$cust_pkg} = [] if $pkgpart;
2196 my $error = $cust_main->insert( \%hash );
2199 $dbh->rollback if $oldAutoCommit;
2200 return "can't insert customer for $line: $error";
2203 #false laziness w/bill.cgi
2204 $error = $cust_main->bill( 'time' => $billtime );
2206 $dbh->rollback if $oldAutoCommit;
2207 return "can't bill customer for $line: $error";
2210 $cust_main->apply_payments;
2211 $cust_main->apply_credits;
2213 $error = $cust_main->collect();
2215 $dbh->rollback if $oldAutoCommit;
2216 return "can't collect customer for $line: $error";
2222 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2224 return "Empty file!" unless $imported;
2236 #warn join('-',keys %$param);
2237 my $fh = $param->{filehandle};
2238 my @fields = @{$param->{fields}};
2240 eval "use Date::Parse;";
2242 eval "use Text::CSV_XS;";
2245 my $csv = new Text::CSV_XS;
2252 local $SIG{HUP} = 'IGNORE';
2253 local $SIG{INT} = 'IGNORE';
2254 local $SIG{QUIT} = 'IGNORE';
2255 local $SIG{TERM} = 'IGNORE';
2256 local $SIG{TSTP} = 'IGNORE';
2257 local $SIG{PIPE} = 'IGNORE';
2259 my $oldAutoCommit = $FS::UID::AutoCommit;
2260 local $FS::UID::AutoCommit = 0;
2263 #while ( $columns = $csv->getline($fh) ) {
2265 while ( defined($line=<$fh>) ) {
2267 $csv->parse($line) or do {
2268 $dbh->rollback if $oldAutoCommit;
2269 return "can't parse: ". $csv->error_input();
2272 my @columns = $csv->fields();
2273 #warn join('-',@columns);
2276 foreach my $field ( @fields ) {
2277 $row{$field} = shift @columns;
2280 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2281 unless ( $cust_main ) {
2282 $dbh->rollback if $oldAutoCommit;
2283 return "unknown custnum $row{'custnum'}";
2286 if ( $row{'amount'} > 0 ) {
2287 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2289 $dbh->rollback if $oldAutoCommit;
2293 } elsif ( $row{'amount'} < 0 ) {
2294 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2297 $dbh->rollback if $oldAutoCommit;
2307 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2309 return "Empty file!" unless $imported;
2321 The delete method should possibly take an FS::cust_main object reference
2322 instead of a scalar customer number.
2324 Bill and collect options should probably be passed as references instead of a
2327 There should probably be a configuration file with a list of allowed credit
2330 No multiple currency support (probably a larger project than just this module).
2334 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2335 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2336 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.