4 use vars qw( @ISA $conf $lpr $processor $xaction $E_NoErr $invoice_from
5 $smtpmachine $Debug $bop_processor $bop_login $bop_password
6 $bop_action @bop_options $import );
14 use Business::CreditCard;
15 use FS::UID qw( getotaker dbh );
16 use FS::Record qw( qsearchs qsearch dbdef );
19 use FS::cust_bill_pkg;
22 use FS::cust_pay_batch;
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;
33 @ISA = qw( FS::Record );
40 #ask FS::UID to run this stuff for us later
41 $FS::UID::callback{'FS::cust_main'} = sub {
43 $lpr = $conf->config('lpr');
44 $invoice_from = $conf->config('invoice_from');
45 $smtpmachine = $conf->config('smtpmachine');
47 if ( $conf->exists('cybercash3.2') ) {
49 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
50 require CCMckDirectLib3_2;
52 require CCMckErrno3_2;
53 #qw(MCKGetErrorMessage $E_NoErr);
54 import CCMckErrno3_2 qw($E_NoErr);
57 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
58 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
59 if ( $status != $E_NoErr ) {
60 warn "CCMckLib3_2::InitConfig error:\n";
61 foreach my $key (keys %CCMckLib3_2::Config) {
62 warn " $key => $CCMckLib3_2::Config{$key}\n"
64 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
65 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
67 $processor='cybercash3.2';
68 } elsif ( $conf->exists('business-onlinepayment') ) {
74 ) = $conf->config('business-onlinepayment');
75 $bop_action ||= 'normal authorization';
76 eval "use Business::OnlinePayment";
77 $processor="Business::OnlinePayment::$bop_processor";
83 my ( $hashref, $cache ) = @_;
84 if ( exists $hashref->{'pkgnum'} ) {
85 # #@{ $self->{'_pkgnum'} } = ();
86 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
87 $self->{'_pkgnum'} = $subcache;
88 #push @{ $self->{'_pkgnum'} },
89 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
95 FS::cust_main - Object methods for cust_main records
101 $record = new FS::cust_main \%hash;
102 $record = new FS::cust_main { 'column' => 'value' };
104 $error = $record->insert;
106 $error = $new_record->replace($old_record);
108 $error = $record->delete;
110 $error = $record->check;
112 @cust_pkg = $record->all_pkgs;
114 @cust_pkg = $record->ncancelled_pkgs;
116 @cust_pkg = $record->suspended_pkgs;
118 $error = $record->bill;
119 $error = $record->bill %options;
120 $error = $record->bill 'time' => $time;
122 $error = $record->collect;
123 $error = $record->collect %options;
124 $error = $record->collect 'invoice_time' => $time,
125 'batch_card' => 'yes',
126 'report_badcard' => 'yes',
131 An FS::cust_main object represents a customer. FS::cust_main inherits from
132 FS::Record. The following fields are currently supported:
136 =item custnum - primary key (assigned automatically for new customers)
138 =item agentnum - agent (see L<FS::agent>)
140 =item refnum - referral (see L<FS::part_referral>)
146 =item ss - social security number (optional)
148 =item company - (optional)
152 =item address2 - (optional)
156 =item county - (optional, see L<FS::cust_main_county>)
158 =item state - (see L<FS::cust_main_county>)
162 =item country - (see L<FS::cust_main_county>)
164 =item daytime - phone (optional)
166 =item night - phone (optional)
168 =item fax - phone (optional)
170 =item ship_first - name
172 =item ship_last - name
174 =item ship_company - (optional)
178 =item ship_address2 - (optional)
182 =item ship_county - (optional, see L<FS::cust_main_county>)
184 =item ship_state - (see L<FS::cust_main_county>)
188 =item ship_country - (see L<FS::cust_main_county>)
190 =item ship_daytime - phone (optional)
192 =item ship_night - phone (optional)
194 =item ship_fax - phone (optional)
196 =item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
198 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
200 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
202 =item payname - name on card or billing name
204 =item tax - tax exempt, empty or `Y'
206 =item otaker - order taker (assigned automatically, see L<FS::UID>)
208 =item comments - comments (optional)
218 Creates a new customer. To add the customer to the database, see L<"insert">.
220 Note that this stores the hash reference, not a distinct copy of the hash it
221 points to. You can ask the object for a copy with the I<hash> method.
225 sub table { 'cust_main'; }
227 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
229 Adds this customer to the database. If there is an error, returns the error,
230 otherwise returns false.
232 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
233 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
234 are inserted atomicly, or the transaction is rolled back. Passing an empty
235 hash reference is equivalent to not supplying this parameter. There should be
236 a better explanation of this, but until then, here's an example:
239 tie %hash, 'Tie::RefHash'; #this part is important
241 $cust_pkg => [ $svc_acct ],
244 $cust_main->insert( \%hash );
246 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
247 be set as the invoicing list (see L<"invoicing_list">). Errors return as
248 expected and rollback the entire transaction; it is not necessary to call
249 check_invoicing_list first. The invoicing_list is set after the records in the
250 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
251 invoicing_list destination to the newly-created svc_acct. Here's an example:
253 $cust_main->insert( {}, [ $email, 'POST' ] );
261 local $SIG{HUP} = 'IGNORE';
262 local $SIG{INT} = 'IGNORE';
263 local $SIG{QUIT} = 'IGNORE';
264 local $SIG{TERM} = 'IGNORE';
265 local $SIG{TSTP} = 'IGNORE';
266 local $SIG{PIPE} = 'IGNORE';
268 my $oldAutoCommit = $FS::UID::AutoCommit;
269 local $FS::UID::AutoCommit = 0;
274 if ( $self->payby eq 'PREPAY' ) {
275 $self->payby('BILL');
276 my $prepay_credit = qsearchs(
278 { 'identifier' => $self->payinfo },
282 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
283 unless $prepay_credit;
284 $amount = $prepay_credit->amount;
285 $seconds = $prepay_credit->seconds;
286 my $error = $prepay_credit->delete;
288 $dbh->rollback if $oldAutoCommit;
289 return "removing prepay_credit (transaction rolled back): $error";
293 my $error = $self->SUPER::insert;
295 $dbh->rollback if $oldAutoCommit;
296 return "inserting cust_main record (transaction rolled back): $error";
299 if ( @param ) { # CUST_PKG_HASHREF
300 my $cust_pkgs = shift @param;
301 foreach my $cust_pkg ( keys %$cust_pkgs ) {
302 $cust_pkg->custnum( $self->custnum );
303 $error = $cust_pkg->insert;
305 $dbh->rollback if $oldAutoCommit;
306 return "inserting cust_pkg (transaction rolled back): $error";
308 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
309 $svc_something->pkgnum( $cust_pkg->pkgnum );
310 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
311 $svc_something->seconds( $svc_something->seconds + $seconds );
314 $error = $svc_something->insert;
316 $dbh->rollback if $oldAutoCommit;
317 return "inserting svc_ (transaction rolled back): $error";
324 $dbh->rollback if $oldAutoCommit;
325 return "No svc_acct record to apply pre-paid time";
328 if ( @param ) { # INVOICING_LIST_ARYREF
329 my $invoicing_list = shift @param;
330 $error = $self->check_invoicing_list( $invoicing_list );
332 $dbh->rollback if $oldAutoCommit;
333 return "checking invoicing_list (transaction rolled back): $error";
335 $self->invoicing_list( $invoicing_list );
339 my $cust_credit = new FS::cust_credit {
340 'custnum' => $self->custnum,
343 $error = $cust_credit->insert;
345 $dbh->rollback if $oldAutoCommit;
346 return "inserting credit (transaction rolled back): $error";
350 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
351 $error = $queue->insert($self->getfield('last'), $self->company);
353 $dbh->rollback if $oldAutoCommit;
354 return "queueing job (transaction rolled back): $error";
357 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
358 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
359 $error = $queue->insert($self->getfield('last'), $self->company);
361 $dbh->rollback if $oldAutoCommit;
362 return "queueing job (transaction rolled back): $error";
366 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
371 =item delete NEW_CUSTNUM
373 This deletes the customer. If there is an error, returns the error, otherwise
376 This will completely remove all traces of the customer record. This is not
377 what you want when a customer cancels service; for that, cancel all of the
378 customer's packages (see L<FS::cust_pkg/cancel>).
380 If the customer has any uncancelled packages, you need to pass a new (valid)
381 customer number for those packages to be transferred to. Cancelled packages
382 will be deleted. Did I mention that this is NOT what you want when a customer
383 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
385 You can't delete a customer with invoices (see L<FS::cust_bill>),
386 or credits (see L<FS::cust_credit>) or payments (see L<FS::cust_pay>).
393 local $SIG{HUP} = 'IGNORE';
394 local $SIG{INT} = 'IGNORE';
395 local $SIG{QUIT} = 'IGNORE';
396 local $SIG{TERM} = 'IGNORE';
397 local $SIG{TSTP} = 'IGNORE';
398 local $SIG{PIPE} = 'IGNORE';
400 my $oldAutoCommit = $FS::UID::AutoCommit;
401 local $FS::UID::AutoCommit = 0;
404 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
405 $dbh->rollback if $oldAutoCommit;
406 return "Can't delete a customer with invoices";
408 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
409 $dbh->rollback if $oldAutoCommit;
410 return "Can't delete a customer with credits";
412 if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
413 $dbh->rollback if $oldAutoCommit;
414 return "Can't delete a customer with payments";
417 my @cust_pkg = $self->ncancelled_pkgs;
419 my $new_custnum = shift;
420 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
421 $dbh->rollback if $oldAutoCommit;
422 return "Invalid new customer number: $new_custnum";
424 foreach my $cust_pkg ( @cust_pkg ) {
425 my %hash = $cust_pkg->hash;
426 $hash{'custnum'} = $new_custnum;
427 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
428 my $error = $new_cust_pkg->replace($cust_pkg);
430 $dbh->rollback if $oldAutoCommit;
435 my @cancelled_cust_pkg = $self->all_pkgs;
436 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
437 my $error = $cust_pkg->delete;
439 $dbh->rollback if $oldAutoCommit;
444 foreach my $cust_main_invoice (
445 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
447 my $error = $cust_main_invoice->delete;
449 $dbh->rollback if $oldAutoCommit;
454 my $error = $self->SUPER::delete;
456 $dbh->rollback if $oldAutoCommit;
460 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
465 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
467 Replaces the OLD_RECORD with this one in the database. If there is an error,
468 returns the error, otherwise returns false.
470 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
471 be set as the invoicing list (see L<"invoicing_list">). Errors return as
472 expected and rollback the entire transaction; it is not necessary to call
473 check_invoicing_list first. Here's an example:
475 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
484 local $SIG{HUP} = 'IGNORE';
485 local $SIG{INT} = 'IGNORE';
486 local $SIG{QUIT} = 'IGNORE';
487 local $SIG{TERM} = 'IGNORE';
488 local $SIG{TSTP} = 'IGNORE';
489 local $SIG{PIPE} = 'IGNORE';
491 my $oldAutoCommit = $FS::UID::AutoCommit;
492 local $FS::UID::AutoCommit = 0;
495 my $error = $self->SUPER::replace($old);
498 $dbh->rollback if $oldAutoCommit;
502 if ( @param ) { # INVOICING_LIST_ARYREF
503 my $invoicing_list = shift @param;
504 $error = $self->check_invoicing_list( $invoicing_list );
506 $dbh->rollback if $oldAutoCommit;
509 $self->invoicing_list( $invoicing_list );
512 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
519 Checks all fields to make sure this is a valid customer record. If there is
520 an error, returns the error, otherwise returns false. Called by the insert
529 $self->ut_numbern('custnum')
530 || $self->ut_number('agentnum')
531 || $self->ut_number('refnum')
532 || $self->ut_name('last')
533 || $self->ut_name('first')
534 || $self->ut_textn('company')
535 || $self->ut_text('address1')
536 || $self->ut_textn('address2')
537 || $self->ut_text('city')
538 || $self->ut_textn('county')
539 || $self->ut_textn('state')
540 || $self->ut_country('country')
541 || $self->ut_anything('comments')
542 || $self->ut_numbern('referral_custnum')
544 #barf. need message catalogs. i18n. etc.
545 $error .= "Please select a referral."
546 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
547 return $error if $error;
549 return "Unknown agent"
550 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
552 return "Unknown referral"
553 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
555 return "Unknown referring custnum ". $self->referral_custnum
556 unless ! $self->referral_custnum
557 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
559 if ( $self->ss eq '' ) {
564 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
565 or return "Illegal social security number: ". $self->ss;
566 $self->ss("$1-$2-$3");
570 unless ( qsearchs('cust_main_county', {
571 'country' => $self->country,
574 return "Unknown state/county/country: ".
575 $self->state. "/". $self->county. "/". $self->country
576 unless qsearchs('cust_main_county',{
577 'state' => $self->state,
578 'county' => $self->county,
579 'country' => $self->country,
585 $self->ut_phonen('daytime', $self->country)
586 || $self->ut_phonen('night', $self->country)
587 || $self->ut_phonen('fax', $self->country)
588 || $self->ut_zip('zip', $self->country)
590 return $error if $error;
593 last first company address1 address2 city county state zip
594 country daytime night fax
597 if ( defined $self->dbdef_table->column('ship_last') ) {
598 if ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } @addfields
599 && grep $self->getfield("ship_$_"), grep $_ ne 'state', @addfields
603 $self->ut_name('ship_last')
604 || $self->ut_name('ship_first')
605 || $self->ut_textn('ship_company')
606 || $self->ut_text('ship_address1')
607 || $self->ut_textn('ship_address2')
608 || $self->ut_text('ship_city')
609 || $self->ut_textn('ship_county')
610 || $self->ut_textn('ship_state')
611 || $self->ut_country('ship_country')
613 return $error if $error;
615 #false laziness with above
616 unless ( qsearchs('cust_main_county', {
617 'country' => $self->ship_country,
620 return "Unknown ship_state/ship_county/ship_country: ".
621 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
622 unless qsearchs('cust_main_county',{
623 'state' => $self->ship_state,
624 'county' => $self->ship_county,
625 'country' => $self->ship_country,
631 $self->ut_phonen('ship_daytime', $self->ship_country)
632 || $self->ut_phonen('ship_night', $self->ship_country)
633 || $self->ut_phonen('ship_fax', $self->ship_country)
634 || $self->ut_zip('ship_zip', $self->ship_country)
636 return $error if $error;
638 } else { # ship_ info eq billing info, so don't store dup info in database
639 $self->setfield("ship_$_", '')
640 foreach qw( last first company address1 address2 city county state zip
641 country daytime night fax );
645 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
646 or return "Illegal payby: ". $self->payby;
649 if ( $self->payby eq 'CARD' ) {
651 my $payinfo = $self->payinfo;
653 $payinfo =~ /^(\d{13,16})$/
654 or return "Illegal credit card number: ". $self->payinfo;
656 $self->payinfo($payinfo);
658 or return "Illegal credit card number: ". $self->payinfo;
659 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
661 } elsif ( $self->payby eq 'BILL' ) {
663 $error = $self->ut_textn('payinfo');
664 return "Illegal P.O. number: ". $self->payinfo if $error;
666 } elsif ( $self->payby eq 'COMP' ) {
668 $error = $self->ut_textn('payinfo');
669 return "Illegal comp account issuer: ". $self->payinfo if $error;
671 } elsif ( $self->payby eq 'PREPAY' ) {
673 my $payinfo = $self->payinfo;
674 $payinfo =~ s/\W//g; #anything else would just confuse things
675 $self->payinfo($payinfo);
676 $error = $self->ut_alpha('payinfo');
677 return "Illegal prepayment identifier: ". $self->payinfo if $error;
678 return "Unknown prepayment identifier"
679 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
683 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
684 return "Expriation date required"
685 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
688 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
689 or return "Illegal expiration date: ". $self->paydate;
690 if ( length($2) == 4 ) {
691 $self->paydate("$2-$1-01");
693 $self->paydate("20$2-$1-01");
697 if ( $self->payname eq '' ) {
698 $self->payname( $self->first. " ". $self->getfield('last') );
700 $self->payname =~ /^([\w \,\.\-\']+)$/
701 or return "Illegal billing name: ". $self->payname;
705 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
708 $self->otaker(getotaker);
715 Returns all packages (see L<FS::cust_pkg>) for this customer.
721 if ( $self->{'_pkgnum'} ) {
722 values %{ $self->{'_pkgnum'}->cache };
724 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
728 =item ncancelled_pkgs
730 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
734 sub ncancelled_pkgs {
736 if ( $self->{'_pkgnum'} ) {
737 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
739 @{ [ # force list context
740 qsearch( 'cust_pkg', {
741 'custnum' => $self->custnum,
744 qsearch( 'cust_pkg', {
745 'custnum' => $self->custnum,
754 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
760 grep { $_->susp } $self->ncancelled_pkgs;
763 =item unflagged_suspended_pkgs
765 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
766 customer (thouse packages without the `manual_flag' set).
770 sub unflagged_suspended_pkgs {
772 return $self->suspended_pkgs
773 unless dbdef->table('cust_pkg')->column('manual_flag');
774 grep { ! $_->manual_flag } $self->suspended_pkgs;
777 =item unsuspended_pkgs
779 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
784 sub unsuspended_pkgs {
786 grep { ! $_->susp } $self->ncancelled_pkgs;
791 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
792 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
793 on success or a list of errors.
799 grep { $_->unsuspend } $self->suspended_pkgs;
804 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
805 Always returns a list: an empty list on success or a list of errors.
811 grep { $_->suspend } $self->unsuspended_pkgs;
816 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
817 conjunction with the collect method.
819 Options are passed as name-value pairs.
821 The only currently available option is `time', which bills the customer as if
822 it were that time. It is specified as a UNIX timestamp; see
823 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
824 functions. For example:
828 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
830 If there is an error, returns the error, otherwise returns false.
835 my( $self, %options ) = @_;
836 my $time = $options{'time'} || time;
841 local $SIG{HUP} = 'IGNORE';
842 local $SIG{INT} = 'IGNORE';
843 local $SIG{QUIT} = 'IGNORE';
844 local $SIG{TERM} = 'IGNORE';
845 local $SIG{TSTP} = 'IGNORE';
846 local $SIG{PIPE} = 'IGNORE';
848 my $oldAutoCommit = $FS::UID::AutoCommit;
849 local $FS::UID::AutoCommit = 0;
852 # find the packages which are due for billing, find out how much they are
853 # & generate invoice database.
855 my( $total_setup, $total_recur ) = ( 0, 0 );
856 my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
857 my @cust_bill_pkg = ();
859 foreach my $cust_pkg (
860 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
863 #NO!! next if $cust_pkg->cancel;
864 next if $cust_pkg->getfield('cancel');
866 #? to avoid use of uninitialized value errors... ?
867 $cust_pkg->setfield('bill', '')
868 unless defined($cust_pkg->bill);
870 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
872 #so we don't modify cust_pkg record unnecessarily
873 my $cust_pkg_mod_flag = 0;
874 my %hash = $cust_pkg->hash;
875 my $old_cust_pkg = new FS::cust_pkg \%hash;
879 unless ( $cust_pkg->setup ) {
880 my $setup_prog = $part_pkg->getfield('setup');
881 $setup_prog =~ /^(.*)$/ or do {
882 $dbh->rollback if $oldAutoCommit;
883 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
889 ##$cpt->permit(); #what is necessary?
890 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
891 #$setup = $cpt->reval($setup_prog);
892 $setup = eval $setup_prog;
893 unless ( defined($setup) ) {
894 $dbh->rollback if $oldAutoCommit;
895 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
896 "(expression $setup_prog): $@";
898 $cust_pkg->setfield('setup',$time);
899 $cust_pkg_mod_flag=1;
905 if ( $part_pkg->getfield('freq') > 0 &&
906 ! $cust_pkg->getfield('susp') &&
907 ( $cust_pkg->getfield('bill') || 0 ) < $time
909 my $recur_prog = $part_pkg->getfield('recur');
910 $recur_prog =~ /^(.*)$/ or do {
911 $dbh->rollback if $oldAutoCommit;
912 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
918 ##$cpt->permit(); #what is necessary?
919 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
920 #$recur = $cpt->reval($recur_prog);
921 $recur = eval $recur_prog;
922 unless ( defined($recur) ) {
923 $dbh->rollback if $oldAutoCommit;
924 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
925 "(expression $recur_prog): $@";
927 #change this bit to use Date::Manip? CAREFUL with timezones (see
928 # mailing list archive)
929 #$sdate=$cust_pkg->bill || time;
930 #$sdate=$cust_pkg->bill || $time;
931 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
932 my ($sec,$min,$hour,$mday,$mon,$year) =
933 (localtime($sdate) )[0,1,2,3,4,5];
934 $mon += $part_pkg->getfield('freq');
935 until ( $mon < 12 ) { $mon -= 12; $year++; }
936 $cust_pkg->setfield('bill',
937 timelocal($sec,$min,$hour,$mday,$mon,$year));
938 $cust_pkg_mod_flag = 1;
941 warn "\$setup is undefined" unless defined($setup);
942 warn "\$recur is undefined" unless defined($recur);
943 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
945 if ( $cust_pkg_mod_flag ) {
946 $error=$cust_pkg->replace($old_cust_pkg);
947 if ( $error ) { #just in case
948 $dbh->rollback if $oldAutoCommit;
949 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
951 $setup = sprintf( "%.2f", $setup );
952 $recur = sprintf( "%.2f", $recur );
954 $dbh->rollback if $oldAutoCommit;
955 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
958 $dbh->rollback if $oldAutoCommit;
959 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
961 if ( $setup > 0 || $recur > 0 ) {
962 my $cust_bill_pkg = new FS::cust_bill_pkg ({
963 'pkgnum' => $cust_pkg->pkgnum,
967 'edate' => $cust_pkg->bill,
969 push @cust_bill_pkg, $cust_bill_pkg;
970 $total_setup += $setup;
971 $total_recur += $recur;
972 $taxable_setup += $setup
973 unless $part_pkg->dbdef_table->column('setuptax')
974 || $part_pkg->setuptax =~ /^Y$/i;
975 $taxable_recur += $recur
976 unless $part_pkg->dbdef_table->column('recurtax')
977 || $part_pkg->recurtax =~ /^Y$/i;
983 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
984 my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
986 unless ( @cust_bill_pkg ) {
987 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
991 unless ( $self->tax =~ /Y/i
992 || $self->payby eq 'COMP'
993 || $taxable_charged == 0 ) {
994 my $cust_main_county = qsearchs('cust_main_county',{
995 'state' => $self->state,
996 'county' => $self->county,
997 'country' => $self->country,
999 my $tax = sprintf( "%.2f",
1000 $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1004 $charged = sprintf( "%.2f", $charged+$tax );
1006 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1013 push @cust_bill_pkg, $cust_bill_pkg;
1017 my $cust_bill = new FS::cust_bill ( {
1018 'custnum' => $self->custnum,
1020 'charged' => $charged,
1022 $error = $cust_bill->insert;
1024 $dbh->rollback if $oldAutoCommit;
1025 return "can't create invoice for customer #". $self->custnum. ": $error";
1028 my $invnum = $cust_bill->invnum;
1030 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1032 $cust_bill_pkg->invnum($invnum);
1033 $error = $cust_bill_pkg->insert;
1035 $dbh->rollback if $oldAutoCommit;
1036 return "can't create invoice line item for customer #". $self->custnum.
1041 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1045 =item collect OPTIONS
1047 (Attempt to) collect money for this customer's outstanding invoices (see
1048 L<FS::cust_bill>). Usually used after the bill method.
1050 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1051 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1053 If there is an error, returns the error, otherwise returns false.
1055 Options are passed as name-value pairs.
1057 Currently available options are:
1059 invoice_time - Use this time when deciding when to print invoices and
1060 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>
1061 for conversion functions.
1063 batch_card - Set this true to batch cards (see L<FS::cust_pay_batch>). By
1064 default, cards are processed immediately, which will generate an error if
1065 CyberCash is not installed.
1067 report_badcard - Set this true if you want bad card transactions to
1068 return an error. By default, they don't.
1073 my( $self, %options ) = @_;
1074 my $invoice_time = $options{'invoice_time'} || time;
1077 local $SIG{HUP} = 'IGNORE';
1078 local $SIG{INT} = 'IGNORE';
1079 local $SIG{QUIT} = 'IGNORE';
1080 local $SIG{TERM} = 'IGNORE';
1081 local $SIG{TSTP} = 'IGNORE';
1082 local $SIG{PIPE} = 'IGNORE';
1084 my $oldAutoCommit = $FS::UID::AutoCommit;
1085 local $FS::UID::AutoCommit = 0;
1088 my $balance = $self->balance;
1089 warn "collect: balance $balance" if $Debug;
1090 unless ( $balance > 0 ) { #redundant?????
1091 $dbh->rollback if $oldAutoCommit; #hmm
1095 foreach my $cust_bill (
1096 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1099 #this has to be before next's
1100 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1104 $balance = sprintf( "%.2f", $balance - $amount );
1106 next unless $cust_bill->owed > 0;
1108 # don't try to charge for the same invoice if it's already in a batch
1109 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1111 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1113 next unless $amount > 0;
1115 if ( $self->payby eq 'BILL' ) {
1118 my $since = $invoice_time - ( $cust_bill->_date || 0 );
1119 #warn "$invoice_time ", $cust_bill->_date, " $since";
1120 if ( $since >= 0 #don't print future invoices
1121 && ( $cust_bill->printed * 2592000 ) <= $since
1124 #my @print_text = $cust_bill->print_text; #( date )
1125 my @invoicing_list = $self->invoicing_list;
1126 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
1127 $ENV{SMTPHOSTS} = $smtpmachine;
1128 $ENV{MAILADDRESS} = $invoice_from;
1129 my $header = new Mail::Header ( [
1130 "From: $invoice_from",
1131 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
1132 "Sender: $invoice_from",
1133 "Reply-To: $invoice_from",
1134 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1137 my $message = new Mail::Internet (
1138 'Header' => $header,
1139 'Body' => [ $cust_bill->print_text ], #( date)
1141 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
1143 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
1144 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
1145 print LPR $cust_bill->print_text; #( date )
1147 or die $! ? "Error closing $lpr: $!"
1148 : "Exit status $? from $lpr";
1151 my %hash = $cust_bill->hash;
1153 my $new_cust_bill = new FS::cust_bill(\%hash);
1154 my $error = $new_cust_bill->replace($cust_bill);
1155 warn "Error updating $cust_bill->printed: $error" if $error;
1159 } elsif ( $self->payby eq 'COMP' ) {
1160 my $cust_pay = new FS::cust_pay ( {
1161 'invnum' => $cust_bill->invnum,
1165 'payinfo' => $self->payinfo,
1168 my $error = $cust_pay->insert;
1170 $dbh->rollback if $oldAutoCommit;
1171 return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
1175 } elsif ( $self->payby eq 'CARD' ) {
1177 if ( $options{'batch_card'} ne 'yes' ) {
1179 unless ( $processor ) {
1180 $dbh->rollback if $oldAutoCommit;
1181 return "Real time card processing not enabled!";
1184 my $address = $self->address1;
1185 $address .= ", ". $self->address2 if $self->address2;
1188 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1189 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1192 if ( $processor eq 'cybercash3.2' ) {
1194 #fix exp. date for cybercash
1195 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1196 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1199 my $paybatch = $cust_bill->invnum.
1200 '-' . time2str("%y%m%d%H%M%S", time);
1202 my $payname = $self->payname ||
1203 $self->getfield('first'). ' '. $self->getfield('last');
1206 my $country = $self->country eq 'US' ? 'USA' : $self->country;
1208 my @full_xaction = ( $xaction,
1209 'Order-ID' => $paybatch,
1210 'Amount' => "usd $amount",
1211 'Card-Number' => $self->getfield('payinfo'),
1212 'Card-Name' => $payname,
1213 'Card-Address' => $address,
1214 'Card-City' => $self->getfield('city'),
1215 'Card-State' => $self->getfield('state'),
1216 'Card-Zip' => $self->getfield('zip'),
1217 'Card-Country' => $country,
1222 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1224 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
1225 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
1226 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1227 my $cust_pay = new FS::cust_pay ( {
1228 'invnum' => $cust_bill->invnum,
1232 'payinfo' => $self->payinfo,
1233 'paybatch' => "$processor:$paybatch",
1235 my $error = $cust_pay->insert;
1237 # gah, even with transactions.
1238 $dbh->commit if $oldAutoCommit; #well.
1239 my $e = 'WARNING: Card debited but database not updated - '.
1240 'error applying payment, invnum #' . $cust_bill->invnum.
1241 " (CyberCash Order-ID $paybatch): $error";
1245 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1246 || $options{'report_badcard'} ) {
1247 $dbh->commit if $oldAutoCommit;
1248 return 'Cybercash error, invnum #' .
1249 $cust_bill->invnum. ':'. $result{'MErrMsg'};
1251 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1255 } elsif ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
1257 my $bop_processor = $1;
1259 my($payname, $payfirst, $paylast);
1260 if ( $self->payname ) {
1261 $payname = $self->payname;
1262 $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
1264 $dbh->rollback if $oldAutoCommit;
1265 return "Illegal payname $payname";
1267 ($payfirst, $paylast) = ($1, $2);
1269 $payfirst = $self->getfield('first');
1270 $paylast = $self->getfield('first');
1271 $payname = "$payfirst $paylast";
1274 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1275 if ( $conf->exists('emailinvoiceauto')
1276 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1277 push @invoicing_list, $self->default_invoicing_list;
1279 my $email = $invoicing_list[0];
1281 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
1284 new Business::OnlinePayment( $bop_processor, @bop_options );
1285 $transaction->content(
1287 'login' => $bop_login,
1288 'password' => $bop_password,
1289 'action' => $action1,
1290 'description' => 'Internet Services',
1291 'amount' => $amount,
1292 'invoice_number' => $cust_bill->invnum,
1293 'customer_id' => $self->custnum,
1294 'last_name' => $paylast,
1295 'first_name' => $payfirst,
1297 'address' => $address,
1298 'city' => $self->city,
1299 'state' => $self->state,
1300 'zip' => $self->zip,
1301 'country' => $self->country,
1302 'card_number' => $self->payinfo,
1303 'expiration' => $exp,
1304 'referer' => 'http://cleanwhisker.420.am/',
1307 $transaction->submit();
1309 if ( $transaction->is_success() && $action2 ) {
1310 my $auth = $transaction->authorization;
1311 my $ordernum = $transaction->order_number;
1312 #warn "********* $auth ***********\n";
1313 #warn "********* $ordernum ***********\n";
1315 new Business::OnlinePayment( $bop_processor, @bop_options );
1319 login => $bop_login,
1320 password => $bop_password,
1321 order_number => $ordernum,
1323 authorization => $auth,
1324 description => 'Internet Services',
1329 unless ( $capture->is_success ) {
1330 my $e = "Authorization sucessful but capture failed, invnum #".
1331 $cust_bill->invnum. ': '. $capture->result_code.
1332 ": ". $capture->error_message;
1339 if ( $transaction->is_success() ) {
1341 my $cust_pay = new FS::cust_pay ( {
1342 'invnum' => $cust_bill->invnum,
1346 'payinfo' => $self->payinfo,
1347 'paybatch' => "$processor:". $transaction->authorization,
1349 my $error = $cust_pay->insert;
1351 # gah, even with transactions.
1352 $dbh->commit if $oldAutoCommit; #well.
1353 my $e = 'WARNING: Card debited but database not updated - '.
1354 'error applying payment, invnum #' . $cust_bill->invnum.
1355 " ($processor): $error";
1359 } elsif ( $options{'report_badcard'} ) {
1360 $dbh->commit if $oldAutoCommit;
1361 return "$processor error, invnum #". $cust_bill->invnum. ': '.
1362 $transaction->result_code. ": ". $transaction->error_message;
1364 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1369 $dbh->rollback if $oldAutoCommit;
1370 return "Unknown real-time processor $processor\n";
1373 } else { #batch card
1375 my $cust_pay_batch = new FS::cust_pay_batch ( {
1376 'invnum' => $cust_bill->getfield('invnum'),
1377 'custnum' => $self->getfield('custnum'),
1378 'last' => $self->getfield('last'),
1379 'first' => $self->getfield('first'),
1380 'address1' => $self->getfield('address1'),
1381 'address2' => $self->getfield('address2'),
1382 'city' => $self->getfield('city'),
1383 'state' => $self->getfield('state'),
1384 'zip' => $self->getfield('zip'),
1385 'country' => $self->getfield('country'),
1387 'cardnum' => $self->getfield('payinfo'),
1388 'exp' => $self->getfield('paydate'),
1389 'payname' => $self->getfield('payname'),
1390 'amount' => $amount,
1392 my $error = $cust_pay_batch->insert;
1394 $dbh->rollback if $oldAutoCommit;
1395 return "Error adding to cust_pay_batch: $error";
1401 $dbh->rollback if $oldAutoCommit;
1402 return "Unknown payment type ". $self->payby;
1406 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1413 Returns the total owed for this customer on all invoices
1414 (see L<FS::cust_bill/owed>).
1420 $self->total_owed_date(2145859200); #12/31/2037
1423 =item total_owed_date TIME
1425 Returns the total owed for this customer on all invoices with date earlier than
1426 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1427 see L<Time::Local> and L<Date::Parse> for conversion functions.
1431 sub total_owed_date {
1435 foreach my $cust_bill (
1436 grep { $_->_date <= $time }
1437 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1439 $total_bill += $cust_bill->owed;
1441 sprintf( "%.2f", $total_bill );
1446 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1447 to outstanding invoice balances in chronological order and returns the value
1448 of any remaining unapplied credits available for refund
1449 (see L<FS::cust_refund>).
1456 return 0 unless $self->total_credited;
1458 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1459 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1461 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1462 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1466 foreach my $cust_bill ( @invoices ) {
1469 if ( !defined($credit) || $credit->credited == 0) {
1470 $credit = pop @credits or last;
1473 if ($cust_bill->owed >= $credit->credited) {
1474 $amount=$credit->credited;
1476 $amount=$cust_bill->owed;
1479 my $cust_credit_bill = new FS::cust_credit_bill ( {
1480 'crednum' => $credit->crednum,
1481 'invnum' => $cust_bill->invnum,
1482 'amount' => $amount,
1484 my $error = $cust_credit_bill->insert;
1485 die $error if $error;
1487 redo if ($cust_bill->owed > 0);
1491 return $self->total_credited;
1494 =item apply_payments
1496 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1497 to outstanding invoice balances in chronological order.
1499 #and returns the value of any remaining unapplied payments.
1503 sub apply_payments {
1508 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1509 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1511 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1512 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1516 foreach my $cust_bill ( @invoices ) {
1519 if ( !defined($payment) || $payment->unapplied == 0 ) {
1520 $payment = pop @payments or last;
1523 if ( $cust_bill->owed >= $payment->unapplied ) {
1524 $amount = $payment->unapplied;
1526 $amount = $cust_bill->owed;
1529 my $cust_bill_pay = new FS::cust_bill_pay ( {
1530 'paynum' => $payment->paynum,
1531 'invnum' => $cust_bill->invnum,
1532 'amount' => $amount,
1534 my $error = $cust_bill_pay->insert;
1535 die $error if $error;
1537 redo if ( $cust_bill->owed > 0);
1541 return $self->total_unapplied_payments;
1544 =item total_credited
1546 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1547 customer. See L<FS::cust_credit/credited>.
1551 sub total_credited {
1553 my $total_credit = 0;
1554 foreach my $cust_credit ( qsearch('cust_credit', {
1555 'custnum' => $self->custnum,
1557 $total_credit += $cust_credit->credited;
1559 sprintf( "%.2f", $total_credit );
1562 =item total_unapplied_payments
1564 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1565 See L<FS::cust_pay/unapplied>.
1569 sub total_unapplied_payments {
1571 my $total_unapplied = 0;
1572 foreach my $cust_pay ( qsearch('cust_pay', {
1573 'custnum' => $self->custnum,
1575 $total_unapplied += $cust_pay->unapplied;
1577 sprintf( "%.2f", $total_unapplied );
1582 Returns the balance for this customer (total_owed minus total_credited
1583 minus total_unapplied_payments).
1590 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1594 =item balance_date TIME
1596 Returns the balance for this customer, only considering invoices with date
1597 earlier than TIME (total_owed_date minus total_credited minus
1598 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1599 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1608 $self->total_owed_date($time)
1609 - $self->total_credited
1610 - $self->total_unapplied_payments
1614 =item invoicing_list [ ARRAYREF ]
1616 If an arguement is given, sets these email addresses as invoice recipients
1617 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1618 (except as warnings), so use check_invoicing_list first.
1620 Returns a list of email addresses (with svcnum entries expanded).
1622 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1623 check it without disturbing anything by passing nothing.
1625 This interface may change in the future.
1629 sub invoicing_list {
1630 my( $self, $arrayref ) = @_;
1632 my @cust_main_invoice;
1633 if ( $self->custnum ) {
1634 @cust_main_invoice =
1635 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1637 @cust_main_invoice = ();
1639 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1640 #warn $cust_main_invoice->destnum;
1641 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1642 #warn $cust_main_invoice->destnum;
1643 my $error = $cust_main_invoice->delete;
1644 warn $error if $error;
1647 if ( $self->custnum ) {
1648 @cust_main_invoice =
1649 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1651 @cust_main_invoice = ();
1653 my %seen = map { $_->address => 1 } @cust_main_invoice;
1654 foreach my $address ( @{$arrayref} ) {
1655 #unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1656 next if exists $seen{$address} && $seen{$address};
1657 $seen{$address} = 1;
1658 my $cust_main_invoice = new FS::cust_main_invoice ( {
1659 'custnum' => $self->custnum,
1662 my $error = $cust_main_invoice->insert;
1663 warn $error if $error;
1666 if ( $self->custnum ) {
1668 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1674 =item check_invoicing_list ARRAYREF
1676 Checks these arguements as valid input for the invoicing_list method. If there
1677 is an error, returns the error, otherwise returns false.
1681 sub check_invoicing_list {
1682 my( $self, $arrayref ) = @_;
1683 foreach my $address ( @{$arrayref} ) {
1684 my $cust_main_invoice = new FS::cust_main_invoice ( {
1685 'custnum' => $self->custnum,
1688 my $error = $self->custnum
1689 ? $cust_main_invoice->check
1690 : $cust_main_invoice->checkdest
1692 return $error if $error;
1697 =item default_invoicing_list
1699 Returns the email addresses of any
1703 sub default_invoicing_list {
1706 foreach my $cust_pkg ( $self->all_pkgs ) {
1707 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1709 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1710 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1712 push @list, map { $_->email } @svc_acct;
1714 $self->invoicing_list(\@list);
1717 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1719 Returns an array of customers referred by this customer (referral_custnum set
1720 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1721 customers referred by customers referred by this customer and so on, inclusive.
1722 The default behavior is DEPTH 1 (no recursion).
1726 sub referral_cust_main {
1728 my $depth = @_ ? shift : 1;
1729 my $exclude = @_ ? shift : {};
1732 map { $exclude->{$_->custnum}++; $_; }
1733 grep { ! $exclude->{ $_->custnum } }
1734 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1738 map { $_->referral_cust_main($depth-1, $exclude) }
1745 =item referral_cust_pkg [ DEPTH ]
1747 Like referral_cust_main, except returns a flat list of all unsuspended packages
1748 for each customer. The number of items in this list may be useful for
1749 comission calculations (perhaps after a grep).
1753 sub referral_cust_pkg {
1755 my $depth = @_ ? shift : 1;
1757 map { $_->unsuspended_pkgs }
1758 grep { $_->unsuspended_pkgs }
1759 $self->referral_cust_main($depth);
1762 =item credit AMOUNT, REASON
1764 Applies a credit to this customer. If there is an error, returns the error,
1765 otherwise returns false.
1770 my( $self, $amount, $reason ) = @_;
1771 my $cust_credit = new FS::cust_credit {
1772 'custnum' => $self->custnum,
1773 'amount' => $amount,
1774 'reason' => $reason,
1776 $cust_credit->insert;
1779 =item charge AMOUNT PKG COMMENT
1781 Creates a one-time charge for this customer. If there is an error, returns
1782 the error, otherwise returns false.
1787 my ( $self, $amount, $pkg, $comment ) = @_;
1789 my $part_pkg = new FS::part_pkg ( {
1790 'pkg' => $pkg || 'One-time charge',
1791 'comment' => $comment,
1808 =item check_and_rebuild_fuzzyfiles
1812 sub check_and_rebuild_fuzzyfiles {
1813 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1814 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1815 or &rebuild_fuzzyfiles;
1818 =item rebuild_fuzzyfiles
1822 sub rebuild_fuzzyfiles {
1824 use Fcntl qw(:flock);
1826 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1830 open(LASTLOCK,">>$dir/cust_main.last")
1831 or die "can't open $dir/cust_main.last: $!";
1832 flock(LASTLOCK,LOCK_EX)
1833 or die "can't lock $dir/cust_main.last: $!";
1835 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1837 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1838 if defined dbdef->table('cust_main')->column('ship_last');
1840 open (LASTCACHE,">$dir/cust_main.last.tmp")
1841 or die "can't open $dir/cust_main.last.tmp: $!";
1842 print LASTCACHE join("\n", @all_last), "\n";
1843 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1845 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1850 open(COMPANYLOCK,">>$dir/cust_main.company")
1851 or die "can't open $dir/cust_main.company: $!";
1852 flock(COMPANYLOCK,LOCK_EX)
1853 or die "can't lock $dir/cust_main.company: $!";
1855 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1857 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1858 if defined dbdef->table('cust_main')->column('ship_last');
1860 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1861 or die "can't open $dir/cust_main.company.tmp: $!";
1862 print COMPANYCACHE join("\n", @all_company), "\n";
1863 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1865 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1875 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1876 open(LASTCACHE,"<$dir/cust_main.last")
1877 or die "can't open $dir/cust_main.last: $!";
1878 my @array = map { chomp; $_; } <LASTCACHE>;
1888 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1889 open(COMPANYCACHE,"<$dir/cust_main.company")
1890 or die "can't open $dir/cust_main.last: $!";
1891 my @array = map { chomp; $_; } <COMPANYCACHE>;
1896 =item append_fuzzyfiles LASTNAME COMPANY
1900 sub append_fuzzyfiles {
1901 my( $last, $company ) = @_;
1903 &check_and_rebuild_fuzzyfiles;
1905 use Fcntl qw(:flock);
1907 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1911 open(LAST,">>$dir/cust_main.last")
1912 or die "can't open $dir/cust_main.last: $!";
1914 or die "can't lock $dir/cust_main.last: $!";
1916 print LAST "$last\n";
1919 or die "can't unlock $dir/cust_main.last: $!";
1925 open(COMPANY,">>$dir/cust_main.company")
1926 or die "can't open $dir/cust_main.company: $!";
1927 flock(COMPANY,LOCK_EX)
1928 or die "can't lock $dir/cust_main.company: $!";
1930 print COMPANY "$company\n";
1932 flock(COMPANY,LOCK_UN)
1933 or die "can't unlock $dir/cust_main.company: $!";
1943 $Id: cust_main.pm,v 1.52 2001-12-28 14:40:35 ivan Exp $
1949 The delete method should possibly take an FS::cust_main object reference
1950 instead of a scalar customer number.
1952 Bill and collect options should probably be passed as references instead of a
1955 CyberCash v2 forces us to define some variables in package main.
1957 There should probably be a configuration file with a list of allowed credit
1960 No multiple currency support (probably a larger project than just this module).
1964 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1965 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1966 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1967 L<FS::UID>, schema.html from the base documentation.