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;
24 use FS::part_referral;
25 use FS::cust_main_county;
27 use FS::cust_main_invoice;
28 use FS::cust_credit_bill;
29 use FS::cust_bill_pay;
30 use FS::prepay_credit;
33 use FS::part_bill_event;
34 use FS::cust_bill_event;
35 use FS::cust_tax_exempt;
37 use FS::Msgcat qw(gettext);
39 @ISA = qw( FS::Record );
46 #ask FS::UID to run this stuff for us later
47 $FS::UID::callback{'FS::cust_main'} = sub {
49 #yes, need it for stuff below (prolly should be cached)
54 my ( $hashref, $cache ) = @_;
55 if ( exists $hashref->{'pkgnum'} ) {
56 # #@{ $self->{'_pkgnum'} } = ();
57 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
58 $self->{'_pkgnum'} = $subcache;
59 #push @{ $self->{'_pkgnum'} },
60 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
66 FS::cust_main - Object methods for cust_main records
72 $record = new FS::cust_main \%hash;
73 $record = new FS::cust_main { 'column' => 'value' };
75 $error = $record->insert;
77 $error = $new_record->replace($old_record);
79 $error = $record->delete;
81 $error = $record->check;
83 @cust_pkg = $record->all_pkgs;
85 @cust_pkg = $record->ncancelled_pkgs;
87 @cust_pkg = $record->suspended_pkgs;
89 $error = $record->bill;
90 $error = $record->bill %options;
91 $error = $record->bill 'time' => $time;
93 $error = $record->collect;
94 $error = $record->collect %options;
95 $error = $record->collect 'invoice_time' => $time,
96 'batch_card' => 'yes',
97 'report_badcard' => 'yes',
102 An FS::cust_main object represents a customer. FS::cust_main inherits from
103 FS::Record. The following fields are currently supported:
107 =item custnum - primary key (assigned automatically for new customers)
109 =item agentnum - agent (see L<FS::agent>)
111 =item refnum - Advertising source (see L<FS::part_referral>)
117 =item ss - social security number (optional)
119 =item company - (optional)
123 =item address2 - (optional)
127 =item county - (optional, see L<FS::cust_main_county>)
129 =item state - (see L<FS::cust_main_county>)
133 =item country - (see L<FS::cust_main_county>)
135 =item daytime - phone (optional)
137 =item night - phone (optional)
139 =item fax - phone (optional)
141 =item ship_first - name
143 =item ship_last - name
145 =item ship_company - (optional)
149 =item ship_address2 - (optional)
153 =item ship_county - (optional, see L<FS::cust_main_county>)
155 =item ship_state - (see L<FS::cust_main_county>)
159 =item ship_country - (see L<FS::cust_main_county>)
161 =item ship_daytime - phone (optional)
163 =item ship_night - phone (optional)
165 =item ship_fax - phone (optional)
167 =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)
169 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
171 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
173 =item payname - name on card or billing name
175 =item tax - tax exempt, empty or `Y'
177 =item otaker - order taker (assigned automatically, see L<FS::UID>)
179 =item comments - comments (optional)
189 Creates a new customer. To add the customer to the database, see L<"insert">.
191 Note that this stores the hash reference, not a distinct copy of the hash it
192 points to. You can ask the object for a copy with the I<hash> method.
196 sub table { 'cust_main'; }
198 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
200 Adds this customer to the database. If there is an error, returns the error,
201 otherwise returns false.
203 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
204 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
205 are inserted atomicly, or the transaction is rolled back. Passing an empty
206 hash reference is equivalent to not supplying this parameter. There should be
207 a better explanation of this, but until then, here's an example:
210 tie %hash, 'Tie::RefHash'; #this part is important
212 $cust_pkg => [ $svc_acct ],
215 $cust_main->insert( \%hash );
217 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
218 be set as the invoicing list (see L<"invoicing_list">). Errors return as
219 expected and rollback the entire transaction; it is not necessary to call
220 check_invoicing_list first. The invoicing_list is set after the records in the
221 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
222 invoicing_list destination to the newly-created svc_acct. Here's an example:
224 $cust_main->insert( {}, [ $email, 'POST' ] );
226 Currently available options are: I<depend_jobnum> and I<noexport>.
228 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
229 on the supplied jobnum (they will not run until the specific job completes).
230 This can be used to defer provisioning until some action completes (such
231 as running the customer's credit card sucessfully).
233 The I<noexport> option is deprecated. If I<noexport> is set true, no
234 provisioning jobs (exports) are scheduled. (You can schedule them later with
235 the B<reexport> method.)
241 my $cust_pkgs = @_ ? shift : {};
242 my $invoicing_list = @_ ? shift : '';
244 warn "FS::cust_main::insert called with options ".
245 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
248 local $SIG{HUP} = 'IGNORE';
249 local $SIG{INT} = 'IGNORE';
250 local $SIG{QUIT} = 'IGNORE';
251 local $SIG{TERM} = 'IGNORE';
252 local $SIG{TSTP} = 'IGNORE';
253 local $SIG{PIPE} = 'IGNORE';
255 my $oldAutoCommit = $FS::UID::AutoCommit;
256 local $FS::UID::AutoCommit = 0;
261 if ( $self->payby eq 'PREPAY' ) {
262 $self->payby('BILL');
263 my $prepay_credit = qsearchs(
265 { 'identifier' => $self->payinfo },
269 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
270 unless $prepay_credit;
271 $amount = $prepay_credit->amount;
272 $seconds = $prepay_credit->seconds;
273 my $error = $prepay_credit->delete;
275 $dbh->rollback if $oldAutoCommit;
276 return "removing prepay_credit (transaction rolled back): $error";
280 my $error = $self->SUPER::insert;
282 $dbh->rollback if $oldAutoCommit;
283 #return "inserting cust_main record (transaction rolled back): $error";
288 if ( $invoicing_list ) {
289 $error = $self->check_invoicing_list( $invoicing_list );
291 $dbh->rollback if $oldAutoCommit;
292 return "checking invoicing_list (transaction rolled back): $error";
294 $self->invoicing_list( $invoicing_list );
298 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
300 $dbh->rollback if $oldAutoCommit;
305 $dbh->rollback if $oldAutoCommit;
306 return "No svc_acct record to apply pre-paid time";
310 my $cust_credit = new FS::cust_credit {
311 'custnum' => $self->custnum,
314 $error = $cust_credit->insert;
316 $dbh->rollback if $oldAutoCommit;
317 return "inserting credit (transaction rolled back): $error";
321 $error = $self->queue_fuzzyfiles_update;
323 $dbh->rollback if $oldAutoCommit;
324 return "updating fuzzy search cache: $error";
327 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
332 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
334 Like the insert method on an existing record, this method orders a package
335 and included services atomicaly. Pass a Tie::RefHash data structure to this
336 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
337 be a better explanation of this, but until then, here's an example:
340 tie %hash, 'Tie::RefHash'; #this part is important
342 $cust_pkg => [ $svc_acct ],
345 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
347 Currently available options are: I<depend_jobnum> and I<noexport>.
349 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
350 on the supplied jobnum (they will not run until the specific job completes).
351 This can be used to defer provisioning until some action completes (such
352 as running the customer's credit card sucessfully).
354 The I<noexport> option is deprecated. If I<noexport> is set true, no
355 provisioning jobs (exports) are scheduled. (You can schedule them later with
356 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
357 on the cust_main object is not recommended, as existing services will also be
364 my $cust_pkgs = shift;
367 my %svc_options = ();
368 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
369 if exists $options{'depend_jobnum'};
370 warn "FS::cust_main::order_pkgs called with options ".
371 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
374 local $SIG{HUP} = 'IGNORE';
375 local $SIG{INT} = 'IGNORE';
376 local $SIG{QUIT} = 'IGNORE';
377 local $SIG{TERM} = 'IGNORE';
378 local $SIG{TSTP} = 'IGNORE';
379 local $SIG{PIPE} = 'IGNORE';
381 my $oldAutoCommit = $FS::UID::AutoCommit;
382 local $FS::UID::AutoCommit = 0;
385 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
387 foreach my $cust_pkg ( keys %$cust_pkgs ) {
388 $cust_pkg->custnum( $self->custnum );
389 my $error = $cust_pkg->insert;
391 $dbh->rollback if $oldAutoCommit;
392 return "inserting cust_pkg (transaction rolled back): $error";
394 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
395 $svc_something->pkgnum( $cust_pkg->pkgnum );
396 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
397 $svc_something->seconds( $svc_something->seconds + $$seconds );
400 $error = $svc_something->insert(%svc_options);
402 $dbh->rollback if $oldAutoCommit;
403 #return "inserting svc_ (transaction rolled back): $error";
409 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
415 This method is deprecated. See the I<depend_jobnum> option to the insert and
416 order_pkgs methods for a better way to defer provisioning.
418 Re-schedules all exports by calling the B<reexport> method of all associated
419 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
420 otherwise returns false.
427 carp "warning: FS::cust_main::reexport is deprectated; ".
428 "use the depend_jobnum option to insert or order_pkgs to delay export";
430 local $SIG{HUP} = 'IGNORE';
431 local $SIG{INT} = 'IGNORE';
432 local $SIG{QUIT} = 'IGNORE';
433 local $SIG{TERM} = 'IGNORE';
434 local $SIG{TSTP} = 'IGNORE';
435 local $SIG{PIPE} = 'IGNORE';
437 my $oldAutoCommit = $FS::UID::AutoCommit;
438 local $FS::UID::AutoCommit = 0;
441 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
442 my $error = $cust_pkg->reexport;
444 $dbh->rollback if $oldAutoCommit;
449 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
454 =item delete NEW_CUSTNUM
456 This deletes the customer. If there is an error, returns the error, otherwise
459 This will completely remove all traces of the customer record. This is not
460 what you want when a customer cancels service; for that, cancel all of the
461 customer's packages (see L</cancel>).
463 If the customer has any uncancelled packages, you need to pass a new (valid)
464 customer number for those packages to be transferred to. Cancelled packages
465 will be deleted. Did I mention that this is NOT what you want when a customer
466 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
468 You can't delete a customer with invoices (see L<FS::cust_bill>),
469 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
470 refunds (see L<FS::cust_refund>).
477 local $SIG{HUP} = 'IGNORE';
478 local $SIG{INT} = 'IGNORE';
479 local $SIG{QUIT} = 'IGNORE';
480 local $SIG{TERM} = 'IGNORE';
481 local $SIG{TSTP} = 'IGNORE';
482 local $SIG{PIPE} = 'IGNORE';
484 my $oldAutoCommit = $FS::UID::AutoCommit;
485 local $FS::UID::AutoCommit = 0;
488 if ( $self->cust_bill ) {
489 $dbh->rollback if $oldAutoCommit;
490 return "Can't delete a customer with invoices";
492 if ( $self->cust_credit ) {
493 $dbh->rollback if $oldAutoCommit;
494 return "Can't delete a customer with credits";
496 if ( $self->cust_pay ) {
497 $dbh->rollback if $oldAutoCommit;
498 return "Can't delete a customer with payments";
500 if ( $self->cust_refund ) {
501 $dbh->rollback if $oldAutoCommit;
502 return "Can't delete a customer with refunds";
505 my @cust_pkg = $self->ncancelled_pkgs;
507 my $new_custnum = shift;
508 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
509 $dbh->rollback if $oldAutoCommit;
510 return "Invalid new customer number: $new_custnum";
512 foreach my $cust_pkg ( @cust_pkg ) {
513 my %hash = $cust_pkg->hash;
514 $hash{'custnum'} = $new_custnum;
515 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
516 my $error = $new_cust_pkg->replace($cust_pkg);
518 $dbh->rollback if $oldAutoCommit;
523 my @cancelled_cust_pkg = $self->all_pkgs;
524 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
525 my $error = $cust_pkg->delete;
527 $dbh->rollback if $oldAutoCommit;
532 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
533 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
535 my $error = $cust_main_invoice->delete;
537 $dbh->rollback if $oldAutoCommit;
542 my $error = $self->SUPER::delete;
544 $dbh->rollback if $oldAutoCommit;
548 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
553 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
555 Replaces the OLD_RECORD with this one in the database. If there is an error,
556 returns the error, otherwise returns false.
558 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
559 be set as the invoicing list (see L<"invoicing_list">). Errors return as
560 expected and rollback the entire transaction; it is not necessary to call
561 check_invoicing_list first. Here's an example:
563 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
572 local $SIG{HUP} = 'IGNORE';
573 local $SIG{INT} = 'IGNORE';
574 local $SIG{QUIT} = 'IGNORE';
575 local $SIG{TERM} = 'IGNORE';
576 local $SIG{TSTP} = 'IGNORE';
577 local $SIG{PIPE} = 'IGNORE';
579 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
580 && $conf->config('users-allow_comp') ) {
581 return "You are not permitted to create complimentary accounts."
582 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
585 my $oldAutoCommit = $FS::UID::AutoCommit;
586 local $FS::UID::AutoCommit = 0;
589 my $error = $self->SUPER::replace($old);
592 $dbh->rollback if $oldAutoCommit;
596 if ( @param ) { # INVOICING_LIST_ARYREF
597 my $invoicing_list = shift @param;
598 $error = $self->check_invoicing_list( $invoicing_list );
600 $dbh->rollback if $oldAutoCommit;
603 $self->invoicing_list( $invoicing_list );
606 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
607 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
608 # card/check/lec info has changed, want to retry realtime_ invoice events
609 my $error = $self->retry_realtime;
611 $dbh->rollback if $oldAutoCommit;
616 $error = $self->queue_fuzzyfiles_update;
618 $dbh->rollback if $oldAutoCommit;
619 return "updating fuzzy search cache: $error";
622 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
627 =item queue_fuzzyfiles_update
629 Used by insert & replace to update the fuzzy search cache
633 sub queue_fuzzyfiles_update {
636 local $SIG{HUP} = 'IGNORE';
637 local $SIG{INT} = 'IGNORE';
638 local $SIG{QUIT} = 'IGNORE';
639 local $SIG{TERM} = 'IGNORE';
640 local $SIG{TSTP} = 'IGNORE';
641 local $SIG{PIPE} = 'IGNORE';
643 my $oldAutoCommit = $FS::UID::AutoCommit;
644 local $FS::UID::AutoCommit = 0;
647 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
648 my $error = $queue->insert($self->getfield('last'), $self->company);
650 $dbh->rollback if $oldAutoCommit;
651 return "queueing job (transaction rolled back): $error";
654 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
655 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
656 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
658 $dbh->rollback if $oldAutoCommit;
659 return "queueing job (transaction rolled back): $error";
663 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
670 Checks all fields to make sure this is a valid customer record. If there is
671 an error, returns the error, otherwise returns false. Called by the insert
679 #warn "BEFORE: \n". $self->_dump;
682 $self->ut_numbern('custnum')
683 || $self->ut_number('agentnum')
684 || $self->ut_number('refnum')
685 || $self->ut_name('last')
686 || $self->ut_name('first')
687 || $self->ut_textn('company')
688 || $self->ut_text('address1')
689 || $self->ut_textn('address2')
690 || $self->ut_text('city')
691 || $self->ut_textn('county')
692 || $self->ut_textn('state')
693 || $self->ut_country('country')
694 || $self->ut_anything('comments')
695 || $self->ut_numbern('referral_custnum')
697 #barf. need message catalogs. i18n. etc.
698 $error .= "Please select an advertising source."
699 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
700 return $error if $error;
702 return "Unknown agent"
703 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
705 return "Unknown refnum"
706 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
708 return "Unknown referring custnum ". $self->referral_custnum
709 unless ! $self->referral_custnum
710 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
712 if ( $self->ss eq '' ) {
717 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
718 or return "Illegal social security number: ". $self->ss;
719 $self->ss("$1-$2-$3");
723 # bad idea to disable, causes billing to fail because of no tax rates later
724 # unless ( $import ) {
725 unless ( qsearch('cust_main_county', {
726 'country' => $self->country,
729 return "Unknown state/county/country: ".
730 $self->state. "/". $self->county. "/". $self->country
731 unless qsearch('cust_main_county',{
732 'state' => $self->state,
733 'county' => $self->county,
734 'country' => $self->country,
740 $self->ut_phonen('daytime', $self->country)
741 || $self->ut_phonen('night', $self->country)
742 || $self->ut_phonen('fax', $self->country)
743 || $self->ut_zip('zip', $self->country)
745 return $error if $error;
748 last first company address1 address2 city county state zip
749 country daytime night fax
752 if ( defined $self->dbdef_table->column('ship_last') ) {
753 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
755 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
759 $self->ut_name('ship_last')
760 || $self->ut_name('ship_first')
761 || $self->ut_textn('ship_company')
762 || $self->ut_text('ship_address1')
763 || $self->ut_textn('ship_address2')
764 || $self->ut_text('ship_city')
765 || $self->ut_textn('ship_county')
766 || $self->ut_textn('ship_state')
767 || $self->ut_country('ship_country')
769 return $error if $error;
771 #false laziness with above
772 unless ( qsearchs('cust_main_county', {
773 'country' => $self->ship_country,
776 return "Unknown ship_state/ship_county/ship_country: ".
777 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
778 unless qsearchs('cust_main_county',{
779 'state' => $self->ship_state,
780 'county' => $self->ship_county,
781 'country' => $self->ship_country,
787 $self->ut_phonen('ship_daytime', $self->ship_country)
788 || $self->ut_phonen('ship_night', $self->ship_country)
789 || $self->ut_phonen('ship_fax', $self->ship_country)
790 || $self->ut_zip('ship_zip', $self->ship_country)
792 return $error if $error;
794 } else { # ship_ info eq billing info, so don't store dup info in database
795 $self->setfield("ship_$_", '')
796 foreach qw( last first company address1 address2 city county state zip
797 country daytime night fax );
801 $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP|PREPAY)$/
802 or return "Illegal payby: ". $self->payby;
805 if ( $self->payby eq 'CARD' ) {
807 my $payinfo = $self->payinfo;
809 $payinfo =~ /^(\d{13,16})$/
810 or return gettext('invalid_card'); # . ": ". $self->payinfo;
812 $self->payinfo($payinfo);
814 or return gettext('invalid_card'); # . ": ". $self->payinfo;
815 return gettext('unknown_card_type')
816 if cardtype($self->payinfo) eq "Unknown";
817 if ( defined $self->dbdef_table->column('paycvv') ) {
818 if ( length($self->paycvv) ) {
819 if ( cardtype($self->payinfo) eq 'American Express card' ) {
820 $self->paycvv =~ /^(\d{4})$/
821 or return "CVV2 (CID) for American Express cards is four digits.";
824 $self->paycvv =~ /^(\d{3})$/
825 or return "CVV2 (CVC2/CID) is three digits.";
833 } elsif ( $self->payby eq 'CHEK' ) {
835 my $payinfo = $self->payinfo;
836 $payinfo =~ s/[^\d\@]//g;
837 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
839 $self->payinfo($payinfo);
840 $self->paycvv('') if $self->dbdef_table->column('paycvv');
842 } elsif ( $self->payby eq 'LECB' ) {
844 my $payinfo = $self->payinfo;
846 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
848 $self->payinfo($payinfo);
849 $self->paycvv('') if $self->dbdef_table->column('paycvv');
851 } elsif ( $self->payby eq 'BILL' ) {
853 $error = $self->ut_textn('payinfo');
854 return "Illegal P.O. number: ". $self->payinfo if $error;
855 $self->paycvv('') if $self->dbdef_table->column('paycvv');
857 } elsif ( $self->payby eq 'COMP' ) {
859 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
860 return "You are not permitted to create complimentary accounts."
861 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
864 $error = $self->ut_textn('payinfo');
865 return "Illegal comp account issuer: ". $self->payinfo if $error;
866 $self->paycvv('') if $self->dbdef_table->column('paycvv');
868 } elsif ( $self->payby eq 'PREPAY' ) {
870 my $payinfo = $self->payinfo;
871 $payinfo =~ s/\W//g; #anything else would just confuse things
872 $self->payinfo($payinfo);
873 $error = $self->ut_alpha('payinfo');
874 return "Illegal prepayment identifier: ". $self->payinfo if $error;
875 return "Unknown prepayment identifier"
876 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
877 $self->paycvv('') if $self->dbdef_table->column('paycvv');
881 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
882 return "Expriation date required"
883 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
886 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
887 or return "Illegal expiration date: ". $self->paydate;
888 my $y = length($2) == 4 ? $2 : "20$2";
889 $self->paydate("$y-$1-01");
890 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
891 return gettext('expired_card')
892 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
895 if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
896 ( ! $conf->exists('require_cardname') || $self->payby ne 'CARD' ) ) {
897 $self->payname( $self->first. " ". $self->getfield('last') );
899 $self->payname =~ /^([\w \,\.\-\']+)$/
900 or return gettext('illegal_name'). " payname: ". $self->payname;
904 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
907 $self->otaker(getotaker) unless $self->otaker;
909 #warn "AFTER: \n". $self->_dump;
916 Returns all packages (see L<FS::cust_pkg>) for this customer.
922 if ( $self->{'_pkgnum'} ) {
923 values %{ $self->{'_pkgnum'}->cache };
925 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
929 =item ncancelled_pkgs
931 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
935 sub ncancelled_pkgs {
937 if ( $self->{'_pkgnum'} ) {
938 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
940 @{ [ # force list context
941 qsearch( 'cust_pkg', {
942 'custnum' => $self->custnum,
945 qsearch( 'cust_pkg', {
946 'custnum' => $self->custnum,
955 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
961 grep { $_->susp } $self->ncancelled_pkgs;
964 =item unflagged_suspended_pkgs
966 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
967 customer (thouse packages without the `manual_flag' set).
971 sub unflagged_suspended_pkgs {
973 return $self->suspended_pkgs
974 unless dbdef->table('cust_pkg')->column('manual_flag');
975 grep { ! $_->manual_flag } $self->suspended_pkgs;
978 =item unsuspended_pkgs
980 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
985 sub unsuspended_pkgs {
987 grep { ! $_->susp } $self->ncancelled_pkgs;
992 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
993 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
994 on success or a list of errors.
1000 grep { $_->unsuspend } $self->suspended_pkgs;
1005 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1006 Always returns a list: an empty list on success or a list of errors.
1012 grep { $_->suspend } $self->unsuspended_pkgs;
1015 =item cancel [ OPTION => VALUE ... ]
1017 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1019 Available options are: I<quiet>
1021 I<quiet> can be set true to supress email cancellation notices.
1023 Always returns a list: an empty list on success or a list of errors.
1029 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1034 Returns the agent (see L<FS::agent>) for this customer.
1040 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1045 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1046 conjunction with the collect method.
1048 Options are passed as name-value pairs.
1050 Currently available options are:
1052 resetup - if set true, re-charges setup fees.
1054 time - bills the customer as if it were that time. Specified as a UNIX
1055 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1056 L<Date::Parse> for conversion functions. For example:
1060 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1063 If there is an error, returns the error, otherwise returns false.
1068 my( $self, %options ) = @_;
1069 my $time = $options{'time'} || time;
1074 local $SIG{HUP} = 'IGNORE';
1075 local $SIG{INT} = 'IGNORE';
1076 local $SIG{QUIT} = 'IGNORE';
1077 local $SIG{TERM} = 'IGNORE';
1078 local $SIG{TSTP} = 'IGNORE';
1079 local $SIG{PIPE} = 'IGNORE';
1081 my $oldAutoCommit = $FS::UID::AutoCommit;
1082 local $FS::UID::AutoCommit = 0;
1085 # find the packages which are due for billing, find out how much they are
1086 # & generate invoice database.
1088 my( $total_setup, $total_recur ) = ( 0, 0 );
1089 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1090 my @cust_bill_pkg = ();
1092 #my $taxable_charged = 0;##
1097 foreach my $cust_pkg (
1098 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1101 #NO!! next if $cust_pkg->cancel;
1102 next if $cust_pkg->getfield('cancel');
1104 #? to avoid use of uninitialized value errors... ?
1105 $cust_pkg->setfield('bill', '')
1106 unless defined($cust_pkg->bill);
1108 my $part_pkg = $cust_pkg->part_pkg;
1110 #so we don't modify cust_pkg record unnecessarily
1111 my $cust_pkg_mod_flag = 0;
1112 my %hash = $cust_pkg->hash;
1113 my $old_cust_pkg = new FS::cust_pkg \%hash;
1117 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1118 my $setup_prog = $part_pkg->getfield('setup');
1119 $setup_prog =~ /^(.*)$/ or do {
1120 $dbh->rollback if $oldAutoCommit;
1121 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
1125 $setup_prog = '0' if $setup_prog =~ /^\s*$/;
1127 #my $cpt = new Safe;
1128 ##$cpt->permit(); #what is necessary?
1129 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1130 #$setup = $cpt->reval($setup_prog);
1131 $setup = eval $setup_prog;
1132 unless ( defined($setup) ) {
1133 $dbh->rollback if $oldAutoCommit;
1134 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1135 "(expression $setup_prog): $@";
1137 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1138 $cust_pkg_mod_flag=1;
1144 if ( $part_pkg->getfield('freq') ne '0' &&
1145 ! $cust_pkg->getfield('susp') &&
1146 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1148 my $recur_prog = $part_pkg->getfield('recur');
1149 $recur_prog =~ /^(.*)$/ or do {
1150 $dbh->rollback if $oldAutoCommit;
1151 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1155 $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1157 # shared with $recur_prog
1158 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1160 #my $cpt = new Safe;
1161 ##$cpt->permit(); #what is necessary?
1162 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1163 #$recur = $cpt->reval($recur_prog);
1164 $recur = eval $recur_prog;
1165 unless ( defined($recur) ) {
1166 $dbh->rollback if $oldAutoCommit;
1167 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1168 "(expression $recur_prog): $@";
1170 #change this bit to use Date::Manip? CAREFUL with timezones (see
1171 # mailing list archive)
1172 my ($sec,$min,$hour,$mday,$mon,$year) =
1173 (localtime($sdate) )[0,1,2,3,4,5];
1175 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1176 # only for figuring next bill date, nothing else, so, reset $sdate again
1178 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1179 $cust_pkg->last_bill($sdate)
1180 if $cust_pkg->dbdef_table->column('last_bill');
1182 if ( $part_pkg->freq =~ /^\d+$/ ) {
1183 $mon += $part_pkg->freq;
1184 until ( $mon < 12 ) { $mon -= 12; $year++; }
1185 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1187 $mday += $weeks * 7;
1188 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1192 $dbh->rollback if $oldAutoCommit;
1193 return "unparsable frequency: ". $part_pkg->freq;
1195 $cust_pkg->setfield('bill',
1196 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1197 $cust_pkg_mod_flag = 1;
1200 warn "\$setup is undefined" unless defined($setup);
1201 warn "\$recur is undefined" unless defined($recur);
1202 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1204 if ( $cust_pkg_mod_flag ) {
1205 $error=$cust_pkg->replace($old_cust_pkg);
1206 if ( $error ) { #just in case
1207 $dbh->rollback if $oldAutoCommit;
1208 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1210 $setup = sprintf( "%.2f", $setup );
1211 $recur = sprintf( "%.2f", $recur );
1212 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1213 $dbh->rollback if $oldAutoCommit;
1214 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1216 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1217 $dbh->rollback if $oldAutoCommit;
1218 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1220 if ( $setup != 0 || $recur != 0 ) {
1221 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1222 'pkgnum' => $cust_pkg->pkgnum,
1226 'edate' => $cust_pkg->bill,
1228 push @cust_bill_pkg, $cust_bill_pkg;
1229 $total_setup += $setup;
1230 $total_recur += $recur;
1232 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1234 my @taxes = qsearch( 'cust_main_county', {
1235 'state' => $self->state,
1236 'county' => $self->county,
1237 'country' => $self->country,
1238 'taxclass' => $part_pkg->taxclass,
1241 @taxes = qsearch( 'cust_main_county', {
1242 'state' => $self->state,
1243 'county' => $self->county,
1244 'country' => $self->country,
1249 #one more try at a whole-country tax rate
1251 @taxes = qsearch( 'cust_main_county', {
1254 'country' => $self->country,
1259 # maybe eliminate this entirely, along with all the 0% records
1261 $dbh->rollback if $oldAutoCommit;
1263 "fatal: can't find tax rate for state/county/country/taxclass ".
1264 join('/', ( map $self->$_(), qw(state county country) ),
1265 $part_pkg->taxclass ). "\n";
1268 foreach my $tax ( @taxes ) {
1270 my $taxable_charged = 0;
1271 $taxable_charged += $setup
1272 unless $part_pkg->setuptax =~ /^Y$/i
1273 || $tax->setuptax =~ /^Y$/i;
1274 $taxable_charged += $recur
1275 unless $part_pkg->recurtax =~ /^Y$/i
1276 || $tax->recurtax =~ /^Y$/i;
1277 next unless $taxable_charged;
1279 if ( $tax->exempt_amount > 0 ) {
1280 my ($mon,$year) = (localtime($sdate) )[4,5];
1282 my $freq = $part_pkg->freq || 1;
1283 if ( $freq !~ /(\d+)$/ ) {
1284 $dbh->rollback if $oldAutoCommit;
1285 return "daily/weekly package definitions not (yet?)".
1286 " compatible with monthly tax exemptions";
1288 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1289 foreach my $which_month ( 1 .. $freq ) {
1291 'custnum' => $self->custnum,
1292 'taxnum' => $tax->taxnum,
1293 'year' => 1900+$year,
1296 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1297 until ( $mon < 13 ) { $mon -= 12; $year++; }
1298 my $cust_tax_exempt =
1299 qsearchs('cust_tax_exempt', \%hash)
1300 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1301 my $remaining_exemption = sprintf("%.2f",
1302 $tax->exempt_amount - $cust_tax_exempt->amount );
1303 if ( $remaining_exemption > 0 ) {
1304 my $addl = $remaining_exemption > $taxable_per_month
1305 ? $taxable_per_month
1306 : $remaining_exemption;
1307 $taxable_charged -= $addl;
1308 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1309 $cust_tax_exempt->hash,
1311 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1313 $error = $new_cust_tax_exempt->exemptnum
1314 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1315 : $new_cust_tax_exempt->insert;
1317 $dbh->rollback if $oldAutoCommit;
1318 return "fatal: can't update cust_tax_exempt: $error";
1321 } # if $remaining_exemption > 0
1323 } #foreach $which_month
1325 } #if $tax->exempt_amount
1327 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1329 #$tax += $taxable_charged * $cust_main_county->tax / 100
1330 $tax{ $tax->taxname || 'Tax' } +=
1331 $taxable_charged * $tax->tax / 100
1333 } #foreach my $tax ( @taxes )
1335 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1337 } #if $setup != 0 || $recur != 0
1339 } #if $cust_pkg_mod_flag
1341 } #foreach my $cust_pkg
1343 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1344 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1346 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1347 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1351 # unless ( $self->tax =~ /Y/i
1352 # || $self->payby eq 'COMP'
1353 # || $taxable_charged == 0 ) {
1354 # my $cust_main_county = qsearchs('cust_main_county',{
1355 # 'state' => $self->state,
1356 # 'county' => $self->county,
1357 # 'country' => $self->country,
1358 # } ) or die "fatal: can't find tax rate for state/county/country ".
1359 # $self->state. "/". $self->county. "/". $self->country. "\n";
1360 # my $tax = sprintf( "%.2f",
1361 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1364 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1366 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1367 my $tax = sprintf("%.2f", $tax{$taxname} );
1368 $charged = sprintf( "%.2f", $charged+$tax );
1370 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1376 'itemdesc' => $taxname,
1378 push @cust_bill_pkg, $cust_bill_pkg;
1381 } else { #1.4 schema
1384 foreach ( values %tax ) { $tax += $_ };
1385 $tax = sprintf("%.2f", $tax);
1387 $charged = sprintf( "%.2f", $charged+$tax );
1389 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1396 push @cust_bill_pkg, $cust_bill_pkg;
1401 my $cust_bill = new FS::cust_bill ( {
1402 'custnum' => $self->custnum,
1404 'charged' => $charged,
1406 $error = $cust_bill->insert;
1408 $dbh->rollback if $oldAutoCommit;
1409 return "can't create invoice for customer #". $self->custnum. ": $error";
1412 my $invnum = $cust_bill->invnum;
1414 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1416 $cust_bill_pkg->invnum($invnum);
1417 $error = $cust_bill_pkg->insert;
1419 $dbh->rollback if $oldAutoCommit;
1420 return "can't create invoice line item for customer #". $self->custnum.
1425 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1429 =item collect OPTIONS
1431 (Attempt to) collect money for this customer's outstanding invoices (see
1432 L<FS::cust_bill>). Usually used after the bill method.
1434 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1435 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1437 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1438 and the invoice events web interface.
1440 If there is an error, returns the error, otherwise returns false.
1442 Options are passed as name-value pairs.
1444 Currently available options are:
1446 invoice_time - Use this time when deciding when to print invoices and
1447 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>
1448 for conversion functions.
1450 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1453 retry_card - Deprecated alias for 'retry'
1455 batch_card - This option is deprecated. See the invoice events web interface
1456 to control whether cards are batched or run against a realtime gateway.
1458 report_badcard - This option is deprecated.
1460 force_print - This option is deprecated; see the invoice events web interface.
1462 quiet - set true to surpress email card/ACH decline notices.
1467 my( $self, %options ) = @_;
1468 my $invoice_time = $options{'invoice_time'} || time;
1471 local $SIG{HUP} = 'IGNORE';
1472 local $SIG{INT} = 'IGNORE';
1473 local $SIG{QUIT} = 'IGNORE';
1474 local $SIG{TERM} = 'IGNORE';
1475 local $SIG{TSTP} = 'IGNORE';
1476 local $SIG{PIPE} = 'IGNORE';
1478 my $oldAutoCommit = $FS::UID::AutoCommit;
1479 local $FS::UID::AutoCommit = 0;
1482 my $balance = $self->balance;
1483 warn "collect customer". $self->custnum. ": balance $balance" if $DEBUG;
1484 unless ( $balance > 0 ) { #redundant?????
1485 $dbh->rollback if $oldAutoCommit; #hmm
1489 if ( exists($options{'retry_card'}) ) {
1490 carp 'retry_card option passed to collect is deprecated; use retry';
1491 $options{'retry'} ||= $options{'retry_card'};
1493 if ( exists($options{'retry'}) && $options{'retry'} ) {
1494 my $error = $self->retry_realtime;
1496 $dbh->rollback if $oldAutoCommit;
1501 foreach my $cust_bill ( $self->open_cust_bill ) {
1503 # don't try to charge for the same invoice if it's already in a batch
1504 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1506 last if $self->balance <= 0;
1508 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
1511 foreach my $part_bill_event (
1512 sort { $a->seconds <=> $b->seconds
1513 || $a->weight <=> $b->weight
1514 || $a->eventpart <=> $b->eventpart }
1515 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1516 && ! qsearchs( 'cust_bill_event', {
1517 'invnum' => $cust_bill->invnum,
1518 'eventpart' => $_->eventpart,
1522 qsearch('part_bill_event', { 'payby' => $self->payby,
1523 'disabled' => '', } )
1526 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
1527 || $self->balance <= 0; # or if balance<=0
1529 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1531 my $cust_main = $self; #for callback
1535 #supress "used only once" warning
1536 $FS::cust_bill::realtime_bop_decline_quiet += 0;
1537 local $FS::cust_bill::realtime_bop_decline_quiet = 1
1538 if $options{'quiet'};
1539 $error = eval $part_bill_event->eventcode;
1543 my $statustext = '';
1547 } elsif ( $error ) {
1549 $statustext = $error;
1554 #add cust_bill_event
1555 my $cust_bill_event = new FS::cust_bill_event {
1556 'invnum' => $cust_bill->invnum,
1557 'eventpart' => $part_bill_event->eventpart,
1558 #'_date' => $invoice_time,
1560 'status' => $status,
1561 'statustext' => $statustext,
1563 $error = $cust_bill_event->insert;
1565 #$dbh->rollback if $oldAutoCommit;
1566 #return "error: $error";
1568 # gah, even with transactions.
1569 $dbh->commit if $oldAutoCommit; #well.
1570 my $e = 'WARNING: Event run but database not updated - '.
1571 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1572 ', eventpart '. $part_bill_event->eventpart.
1583 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1588 =item retry_realtime
1590 Schedules realtime credit card / electronic check / LEC billing events for
1591 for retry. Useful if card information has changed or manual retry is desired.
1592 The 'collect' method must be called to actually retry the transaction.
1594 Implementation details: For each of this customer's open invoices, changes
1595 the status of the first "done" (with statustext error) realtime processing
1600 sub retry_realtime {
1603 local $SIG{HUP} = 'IGNORE';
1604 local $SIG{INT} = 'IGNORE';
1605 local $SIG{QUIT} = 'IGNORE';
1606 local $SIG{TERM} = 'IGNORE';
1607 local $SIG{TSTP} = 'IGNORE';
1608 local $SIG{PIPE} = 'IGNORE';
1610 my $oldAutoCommit = $FS::UID::AutoCommit;
1611 local $FS::UID::AutoCommit = 0;
1614 foreach my $cust_bill (
1615 grep { $_->cust_bill_event }
1616 $self->open_cust_bill
1618 my @cust_bill_event =
1619 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1621 #$_->part_bill_event->plan eq 'realtime-card'
1622 $_->part_bill_event->eventcode =~
1623 /\$cust_bill\->realtime_(card|ach|lec)/
1624 && $_->status eq 'done'
1627 $cust_bill->cust_bill_event;
1628 next unless @cust_bill_event;
1629 my $error = $cust_bill_event[0]->retry;
1631 $dbh->rollback if $oldAutoCommit;
1632 return "error scheduling invoice event for retry: $error";
1637 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1644 Returns the total owed for this customer on all invoices
1645 (see L<FS::cust_bill/owed>).
1651 $self->total_owed_date(2145859200); #12/31/2037
1654 =item total_owed_date TIME
1656 Returns the total owed for this customer on all invoices with date earlier than
1657 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1658 see L<Time::Local> and L<Date::Parse> for conversion functions.
1662 sub total_owed_date {
1666 foreach my $cust_bill (
1667 grep { $_->_date <= $time }
1668 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1670 $total_bill += $cust_bill->owed;
1672 sprintf( "%.2f", $total_bill );
1677 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1678 to outstanding invoice balances in chronological order and returns the value
1679 of any remaining unapplied credits available for refund
1680 (see L<FS::cust_refund>).
1687 return 0 unless $self->total_credited;
1689 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1690 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1692 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1693 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1697 foreach my $cust_bill ( @invoices ) {
1700 if ( !defined($credit) || $credit->credited == 0) {
1701 $credit = pop @credits or last;
1704 if ($cust_bill->owed >= $credit->credited) {
1705 $amount=$credit->credited;
1707 $amount=$cust_bill->owed;
1710 my $cust_credit_bill = new FS::cust_credit_bill ( {
1711 'crednum' => $credit->crednum,
1712 'invnum' => $cust_bill->invnum,
1713 'amount' => $amount,
1715 my $error = $cust_credit_bill->insert;
1716 die $error if $error;
1718 redo if ($cust_bill->owed > 0);
1722 return $self->total_credited;
1725 =item apply_payments
1727 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1728 to outstanding invoice balances in chronological order.
1730 #and returns the value of any remaining unapplied payments.
1734 sub apply_payments {
1739 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1740 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1742 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1743 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1747 foreach my $cust_bill ( @invoices ) {
1750 if ( !defined($payment) || $payment->unapplied == 0 ) {
1751 $payment = pop @payments or last;
1754 if ( $cust_bill->owed >= $payment->unapplied ) {
1755 $amount = $payment->unapplied;
1757 $amount = $cust_bill->owed;
1760 my $cust_bill_pay = new FS::cust_bill_pay ( {
1761 'paynum' => $payment->paynum,
1762 'invnum' => $cust_bill->invnum,
1763 'amount' => $amount,
1765 my $error = $cust_bill_pay->insert;
1766 die $error if $error;
1768 redo if ( $cust_bill->owed > 0);
1772 return $self->total_unapplied_payments;
1775 =item total_credited
1777 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1778 customer. See L<FS::cust_credit/credited>.
1782 sub total_credited {
1784 my $total_credit = 0;
1785 foreach my $cust_credit ( qsearch('cust_credit', {
1786 'custnum' => $self->custnum,
1788 $total_credit += $cust_credit->credited;
1790 sprintf( "%.2f", $total_credit );
1793 =item total_unapplied_payments
1795 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1796 See L<FS::cust_pay/unapplied>.
1800 sub total_unapplied_payments {
1802 my $total_unapplied = 0;
1803 foreach my $cust_pay ( qsearch('cust_pay', {
1804 'custnum' => $self->custnum,
1806 $total_unapplied += $cust_pay->unapplied;
1808 sprintf( "%.2f", $total_unapplied );
1813 Returns the balance for this customer (total_owed minus total_credited
1814 minus total_unapplied_payments).
1821 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1825 =item balance_date TIME
1827 Returns the balance for this customer, only considering invoices with date
1828 earlier than TIME (total_owed_date minus total_credited minus
1829 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1830 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1839 $self->total_owed_date($time)
1840 - $self->total_credited
1841 - $self->total_unapplied_payments
1845 =item invoicing_list [ ARRAYREF ]
1847 If an arguement is given, sets these email addresses as invoice recipients
1848 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1849 (except as warnings), so use check_invoicing_list first.
1851 Returns a list of email addresses (with svcnum entries expanded).
1853 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1854 check it without disturbing anything by passing nothing.
1856 This interface may change in the future.
1860 sub invoicing_list {
1861 my( $self, $arrayref ) = @_;
1863 my @cust_main_invoice;
1864 if ( $self->custnum ) {
1865 @cust_main_invoice =
1866 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1868 @cust_main_invoice = ();
1870 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1871 #warn $cust_main_invoice->destnum;
1872 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1873 #warn $cust_main_invoice->destnum;
1874 my $error = $cust_main_invoice->delete;
1875 warn $error if $error;
1878 if ( $self->custnum ) {
1879 @cust_main_invoice =
1880 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1882 @cust_main_invoice = ();
1884 my %seen = map { $_->address => 1 } @cust_main_invoice;
1885 foreach my $address ( @{$arrayref} ) {
1886 next if exists $seen{$address} && $seen{$address};
1887 $seen{$address} = 1;
1888 my $cust_main_invoice = new FS::cust_main_invoice ( {
1889 'custnum' => $self->custnum,
1892 my $error = $cust_main_invoice->insert;
1893 warn $error if $error;
1896 if ( $self->custnum ) {
1898 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1904 =item check_invoicing_list ARRAYREF
1906 Checks these arguements as valid input for the invoicing_list method. If there
1907 is an error, returns the error, otherwise returns false.
1911 sub check_invoicing_list {
1912 my( $self, $arrayref ) = @_;
1913 foreach my $address ( @{$arrayref} ) {
1914 my $cust_main_invoice = new FS::cust_main_invoice ( {
1915 'custnum' => $self->custnum,
1918 my $error = $self->custnum
1919 ? $cust_main_invoice->check
1920 : $cust_main_invoice->checkdest
1922 return $error if $error;
1927 =item set_default_invoicing_list
1929 Sets the invoicing list to all accounts associated with this customer,
1930 overwriting any previous invoicing list.
1934 sub set_default_invoicing_list {
1936 $self->invoicing_list($self->all_emails);
1941 Returns the email addresses of all accounts provisioned for this customer.
1948 foreach my $cust_pkg ( $self->all_pkgs ) {
1949 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1951 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1952 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1954 $list{$_}=1 foreach map { $_->email } @svc_acct;
1959 =item invoicing_list_addpost
1961 Adds postal invoicing to this customer. If this customer is already configured
1962 to receive postal invoices, does nothing.
1966 sub invoicing_list_addpost {
1968 return if grep { $_ eq 'POST' } $self->invoicing_list;
1969 my @invoicing_list = $self->invoicing_list;
1970 push @invoicing_list, 'POST';
1971 $self->invoicing_list(\@invoicing_list);
1974 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1976 Returns an array of customers referred by this customer (referral_custnum set
1977 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1978 customers referred by customers referred by this customer and so on, inclusive.
1979 The default behavior is DEPTH 1 (no recursion).
1983 sub referral_cust_main {
1985 my $depth = @_ ? shift : 1;
1986 my $exclude = @_ ? shift : {};
1989 map { $exclude->{$_->custnum}++; $_; }
1990 grep { ! $exclude->{ $_->custnum } }
1991 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1995 map { $_->referral_cust_main($depth-1, $exclude) }
2002 =item referral_cust_main_ncancelled
2004 Same as referral_cust_main, except only returns customers with uncancelled
2009 sub referral_cust_main_ncancelled {
2011 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2014 =item referral_cust_pkg [ DEPTH ]
2016 Like referral_cust_main, except returns a flat list of all unsuspended (and
2017 uncancelled) packages for each customer. The number of items in this list may
2018 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2022 sub referral_cust_pkg {
2024 my $depth = @_ ? shift : 1;
2026 map { $_->unsuspended_pkgs }
2027 grep { $_->unsuspended_pkgs }
2028 $self->referral_cust_main($depth);
2031 =item credit AMOUNT, REASON
2033 Applies a credit to this customer. If there is an error, returns the error,
2034 otherwise returns false.
2039 my( $self, $amount, $reason ) = @_;
2040 my $cust_credit = new FS::cust_credit {
2041 'custnum' => $self->custnum,
2042 'amount' => $amount,
2043 'reason' => $reason,
2045 $cust_credit->insert;
2048 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2050 Creates a one-time charge for this customer. If there is an error, returns
2051 the error, otherwise returns false.
2056 my ( $self, $amount ) = ( shift, shift );
2057 my $pkg = @_ ? shift : 'One-time charge';
2058 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2059 my $taxclass = @_ ? shift : '';
2061 local $SIG{HUP} = 'IGNORE';
2062 local $SIG{INT} = 'IGNORE';
2063 local $SIG{QUIT} = 'IGNORE';
2064 local $SIG{TERM} = 'IGNORE';
2065 local $SIG{TSTP} = 'IGNORE';
2066 local $SIG{PIPE} = 'IGNORE';
2068 my $oldAutoCommit = $FS::UID::AutoCommit;
2069 local $FS::UID::AutoCommit = 0;
2072 my $part_pkg = new FS::part_pkg ( {
2074 'comment' => $comment,
2079 'taxclass' => $taxclass,
2082 my $error = $part_pkg->insert;
2084 $dbh->rollback if $oldAutoCommit;
2088 my $pkgpart = $part_pkg->pkgpart;
2089 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2090 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2091 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2092 $error = $type_pkgs->insert;
2094 $dbh->rollback if $oldAutoCommit;
2099 my $cust_pkg = new FS::cust_pkg ( {
2100 'custnum' => $self->custnum,
2101 'pkgpart' => $pkgpart,
2104 $error = $cust_pkg->insert;
2106 $dbh->rollback if $oldAutoCommit;
2110 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2117 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2123 sort { $a->_date <=> $b->_date }
2124 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2127 =item open_cust_bill
2129 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2134 sub open_cust_bill {
2136 grep { $_->owed > 0 } $self->cust_bill;
2141 Returns all the credits (see L<FS::cust_credit>) for this customer.
2147 sort { $a->_date <=> $b->_date }
2148 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
2153 Returns all the payments (see L<FS::cust_pay>) for this customer.
2159 sort { $a->_date <=> $b->_date }
2160 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
2165 Returns all the refunds (see L<FS::cust_refund>) for this customer.
2171 sort { $a->_date <=> $b->_date }
2172 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
2181 =item check_and_rebuild_fuzzyfiles
2185 sub check_and_rebuild_fuzzyfiles {
2186 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2187 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2188 or &rebuild_fuzzyfiles;
2191 =item rebuild_fuzzyfiles
2195 sub rebuild_fuzzyfiles {
2197 use Fcntl qw(:flock);
2199 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2203 open(LASTLOCK,">>$dir/cust_main.last")
2204 or die "can't open $dir/cust_main.last: $!";
2205 flock(LASTLOCK,LOCK_EX)
2206 or die "can't lock $dir/cust_main.last: $!";
2208 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2210 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2211 if defined dbdef->table('cust_main')->column('ship_last');
2213 open (LASTCACHE,">$dir/cust_main.last.tmp")
2214 or die "can't open $dir/cust_main.last.tmp: $!";
2215 print LASTCACHE join("\n", @all_last), "\n";
2216 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2218 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
2223 open(COMPANYLOCK,">>$dir/cust_main.company")
2224 or die "can't open $dir/cust_main.company: $!";
2225 flock(COMPANYLOCK,LOCK_EX)
2226 or die "can't lock $dir/cust_main.company: $!";
2228 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
2230 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
2231 if defined dbdef->table('cust_main')->column('ship_last');
2233 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
2234 or die "can't open $dir/cust_main.company.tmp: $!";
2235 print COMPANYCACHE join("\n", @all_company), "\n";
2236 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
2238 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
2248 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2249 open(LASTCACHE,"<$dir/cust_main.last")
2250 or die "can't open $dir/cust_main.last: $!";
2251 my @array = map { chomp; $_; } <LASTCACHE>;
2261 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2262 open(COMPANYCACHE,"<$dir/cust_main.company")
2263 or die "can't open $dir/cust_main.last: $!";
2264 my @array = map { chomp; $_; } <COMPANYCACHE>;
2269 =item append_fuzzyfiles LASTNAME COMPANY
2273 sub append_fuzzyfiles {
2274 my( $last, $company ) = @_;
2276 &check_and_rebuild_fuzzyfiles;
2278 use Fcntl qw(:flock);
2280 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2284 open(LAST,">>$dir/cust_main.last")
2285 or die "can't open $dir/cust_main.last: $!";
2287 or die "can't lock $dir/cust_main.last: $!";
2289 print LAST "$last\n";
2292 or die "can't unlock $dir/cust_main.last: $!";
2298 open(COMPANY,">>$dir/cust_main.company")
2299 or die "can't open $dir/cust_main.company: $!";
2300 flock(COMPANY,LOCK_EX)
2301 or die "can't lock $dir/cust_main.company: $!";
2303 print COMPANY "$company\n";
2305 flock(COMPANY,LOCK_UN)
2306 or die "can't unlock $dir/cust_main.company: $!";
2320 #warn join('-',keys %$param);
2321 my $fh = $param->{filehandle};
2322 my $agentnum = $param->{agentnum};
2323 my $refnum = $param->{refnum};
2324 my $pkgpart = $param->{pkgpart};
2325 my @fields = @{$param->{fields}};
2327 eval "use Date::Parse;";
2329 eval "use Text::CSV_XS;";
2332 my $csv = new Text::CSV_XS;
2339 local $SIG{HUP} = 'IGNORE';
2340 local $SIG{INT} = 'IGNORE';
2341 local $SIG{QUIT} = 'IGNORE';
2342 local $SIG{TERM} = 'IGNORE';
2343 local $SIG{TSTP} = 'IGNORE';
2344 local $SIG{PIPE} = 'IGNORE';
2346 my $oldAutoCommit = $FS::UID::AutoCommit;
2347 local $FS::UID::AutoCommit = 0;
2350 #while ( $columns = $csv->getline($fh) ) {
2352 while ( defined($line=<$fh>) ) {
2354 $csv->parse($line) or do {
2355 $dbh->rollback if $oldAutoCommit;
2356 return "can't parse: ". $csv->error_input();
2359 my @columns = $csv->fields();
2360 #warn join('-',@columns);
2363 agentnum => $agentnum,
2365 country => 'US', #default
2366 payby => 'BILL', #default
2367 paydate => '12/2037', #default
2369 my $billtime = time;
2370 my %cust_pkg = ( pkgpart => $pkgpart );
2371 foreach my $field ( @fields ) {
2372 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2373 #$cust_pkg{$1} = str2time( shift @$columns );
2374 if ( $1 eq 'setup' ) {
2375 $billtime = str2time(shift @columns);
2377 $cust_pkg{$1} = str2time( shift @columns );
2380 #$cust_main{$field} = shift @$columns;
2381 $cust_main{$field} = shift @columns;
2385 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2386 my $cust_main = new FS::cust_main ( \%cust_main );
2388 tie my %hash, 'Tie::RefHash'; #this part is important
2389 $hash{$cust_pkg} = [] if $pkgpart;
2390 my $error = $cust_main->insert( \%hash );
2393 $dbh->rollback if $oldAutoCommit;
2394 return "can't insert customer for $line: $error";
2397 #false laziness w/bill.cgi
2398 $error = $cust_main->bill( 'time' => $billtime );
2400 $dbh->rollback if $oldAutoCommit;
2401 return "can't bill customer for $line: $error";
2404 $cust_main->apply_payments;
2405 $cust_main->apply_credits;
2407 $error = $cust_main->collect();
2409 $dbh->rollback if $oldAutoCommit;
2410 return "can't collect customer for $line: $error";
2416 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2418 return "Empty file!" unless $imported;
2430 #warn join('-',keys %$param);
2431 my $fh = $param->{filehandle};
2432 my @fields = @{$param->{fields}};
2434 eval "use Date::Parse;";
2436 eval "use Text::CSV_XS;";
2439 my $csv = new Text::CSV_XS;
2446 local $SIG{HUP} = 'IGNORE';
2447 local $SIG{INT} = 'IGNORE';
2448 local $SIG{QUIT} = 'IGNORE';
2449 local $SIG{TERM} = 'IGNORE';
2450 local $SIG{TSTP} = 'IGNORE';
2451 local $SIG{PIPE} = 'IGNORE';
2453 my $oldAutoCommit = $FS::UID::AutoCommit;
2454 local $FS::UID::AutoCommit = 0;
2457 #while ( $columns = $csv->getline($fh) ) {
2459 while ( defined($line=<$fh>) ) {
2461 $csv->parse($line) or do {
2462 $dbh->rollback if $oldAutoCommit;
2463 return "can't parse: ". $csv->error_input();
2466 my @columns = $csv->fields();
2467 #warn join('-',@columns);
2470 foreach my $field ( @fields ) {
2471 $row{$field} = shift @columns;
2474 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2475 unless ( $cust_main ) {
2476 $dbh->rollback if $oldAutoCommit;
2477 return "unknown custnum $row{'custnum'}";
2480 if ( $row{'amount'} > 0 ) {
2481 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2483 $dbh->rollback if $oldAutoCommit;
2487 } elsif ( $row{'amount'} < 0 ) {
2488 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2491 $dbh->rollback if $oldAutoCommit;
2501 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2503 return "Empty file!" unless $imported;
2515 The delete method should possibly take an FS::cust_main object reference
2516 instead of a scalar customer number.
2518 Bill and collect options should probably be passed as references instead of a
2521 There should probably be a configuration file with a list of allowed credit
2524 No multiple currency support (probably a larger project than just this module).
2528 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2529 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2530 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.