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.
1070 force_print - force printing even if invoice has been printed more than once
1071 every 30 days, and don't increment the `printed' field.
1076 my( $self, %options ) = @_;
1077 my $invoice_time = $options{'invoice_time'} || time;
1080 local $SIG{HUP} = 'IGNORE';
1081 local $SIG{INT} = 'IGNORE';
1082 local $SIG{QUIT} = 'IGNORE';
1083 local $SIG{TERM} = 'IGNORE';
1084 local $SIG{TSTP} = 'IGNORE';
1085 local $SIG{PIPE} = 'IGNORE';
1087 my $oldAutoCommit = $FS::UID::AutoCommit;
1088 local $FS::UID::AutoCommit = 0;
1091 my $balance = $self->balance;
1092 warn "collect: balance $balance" if $Debug;
1093 unless ( $balance > 0 ) { #redundant?????
1094 $dbh->rollback if $oldAutoCommit; #hmm
1098 foreach my $cust_bill (
1099 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1102 #this has to be before next's
1103 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1107 $balance = sprintf( "%.2f", $balance - $amount );
1109 next unless $cust_bill->owed > 0;
1111 # don't try to charge for the same invoice if it's already in a batch
1112 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1114 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1116 next unless $amount > 0;
1118 if ( $self->payby eq 'BILL' ) {
1121 my $since = $invoice_time - ( $cust_bill->_date || 0 );
1122 #warn "$invoice_time ", $cust_bill->_date, " $since";
1123 if ( $since >= 0 #don't print future invoices
1124 && ( ( $cust_bill->printed * 2592000 ) <= $since
1125 || $options{'force_print'} )
1128 #my @print_text = $cust_bill->print_text; #( date )
1129 my @invoicing_list = $self->invoicing_list;
1130 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
1131 $ENV{SMTPHOSTS} = $smtpmachine;
1132 $ENV{MAILADDRESS} = $invoice_from;
1133 my $header = new Mail::Header ( [
1134 "From: $invoice_from",
1135 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
1136 "Sender: $invoice_from",
1137 "Reply-To: $invoice_from",
1138 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1141 my $message = new Mail::Internet (
1142 'Header' => $header,
1143 'Body' => [ $cust_bill->print_text ], #( date)
1145 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
1147 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
1148 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
1149 print LPR $cust_bill->print_text; #( date )
1151 or die $! ? "Error closing $lpr: $!"
1152 : "Exit status $? from $lpr";
1155 unless ( $options{'force_print'} ) {
1156 my %hash = $cust_bill->hash;
1158 my $new_cust_bill = new FS::cust_bill(\%hash);
1159 my $error = $new_cust_bill->replace($cust_bill);
1160 warn "Error updating $cust_bill->printed: $error" if $error;
1165 } elsif ( $self->payby eq 'COMP' ) {
1166 my $cust_pay = new FS::cust_pay ( {
1167 'invnum' => $cust_bill->invnum,
1171 'payinfo' => $self->payinfo,
1174 my $error = $cust_pay->insert;
1176 $dbh->rollback if $oldAutoCommit;
1177 return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
1181 } elsif ( $self->payby eq 'CARD' ) {
1183 if ( $options{'batch_card'} ne 'yes' ) {
1185 unless ( $processor ) {
1186 $dbh->rollback if $oldAutoCommit;
1187 return "Real time card processing not enabled!";
1190 my $address = $self->address1;
1191 $address .= ", ". $self->address2 if $self->address2;
1194 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1195 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1198 if ( $processor eq 'cybercash3.2' ) {
1200 #fix exp. date for cybercash
1201 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1202 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1205 my $paybatch = $cust_bill->invnum.
1206 '-' . time2str("%y%m%d%H%M%S", time);
1208 my $payname = $self->payname ||
1209 $self->getfield('first'). ' '. $self->getfield('last');
1212 my $country = $self->country eq 'US' ? 'USA' : $self->country;
1214 my @full_xaction = ( $xaction,
1215 'Order-ID' => $paybatch,
1216 'Amount' => "usd $amount",
1217 'Card-Number' => $self->getfield('payinfo'),
1218 'Card-Name' => $payname,
1219 'Card-Address' => $address,
1220 'Card-City' => $self->getfield('city'),
1221 'Card-State' => $self->getfield('state'),
1222 'Card-Zip' => $self->getfield('zip'),
1223 'Card-Country' => $country,
1228 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1230 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
1231 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
1232 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1233 my $cust_pay = new FS::cust_pay ( {
1234 'invnum' => $cust_bill->invnum,
1238 'payinfo' => $self->payinfo,
1239 'paybatch' => "$processor:$paybatch",
1241 my $error = $cust_pay->insert;
1243 # gah, even with transactions.
1244 $dbh->commit if $oldAutoCommit; #well.
1245 my $e = 'WARNING: Card debited but database not updated - '.
1246 'error applying payment, invnum #' . $cust_bill->invnum.
1247 " (CyberCash Order-ID $paybatch): $error";
1251 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1252 || $options{'report_badcard'} ) {
1253 $dbh->commit if $oldAutoCommit;
1254 return 'Cybercash error, invnum #' .
1255 $cust_bill->invnum. ':'. $result{'MErrMsg'};
1257 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1261 } elsif ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
1263 my $bop_processor = $1;
1265 my($payname, $payfirst, $paylast);
1266 if ( $self->payname ) {
1267 $payname = $self->payname;
1268 $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
1270 $dbh->rollback if $oldAutoCommit;
1271 return "Illegal payname $payname";
1273 ($payfirst, $paylast) = ($1, $2);
1275 $payfirst = $self->getfield('first');
1276 $paylast = $self->getfield('first');
1277 $payname = "$payfirst $paylast";
1280 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1281 if ( $conf->exists('emailinvoiceauto')
1282 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1283 push @invoicing_list, $self->default_invoicing_list;
1285 my $email = $invoicing_list[0];
1287 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
1290 new Business::OnlinePayment( $bop_processor, @bop_options );
1291 $transaction->content(
1293 'login' => $bop_login,
1294 'password' => $bop_password,
1295 'action' => $action1,
1296 'description' => 'Internet Services',
1297 'amount' => $amount,
1298 'invoice_number' => $cust_bill->invnum,
1299 'customer_id' => $self->custnum,
1300 'last_name' => $paylast,
1301 'first_name' => $payfirst,
1303 'address' => $address,
1304 'city' => $self->city,
1305 'state' => $self->state,
1306 'zip' => $self->zip,
1307 'country' => $self->country,
1308 'card_number' => $self->payinfo,
1309 'expiration' => $exp,
1310 'referer' => 'http://cleanwhisker.420.am/',
1313 $transaction->submit();
1315 if ( $transaction->is_success() && $action2 ) {
1316 my $auth = $transaction->authorization;
1317 my $ordernum = $transaction->order_number;
1318 #warn "********* $auth ***********\n";
1319 #warn "********* $ordernum ***********\n";
1321 new Business::OnlinePayment( $bop_processor, @bop_options );
1325 login => $bop_login,
1326 password => $bop_password,
1327 order_number => $ordernum,
1329 authorization => $auth,
1330 description => 'Internet Services',
1335 unless ( $capture->is_success ) {
1336 my $e = "Authorization sucessful but capture failed, invnum #".
1337 $cust_bill->invnum. ': '. $capture->result_code.
1338 ": ". $capture->error_message;
1345 if ( $transaction->is_success() ) {
1347 my $cust_pay = new FS::cust_pay ( {
1348 'invnum' => $cust_bill->invnum,
1352 'payinfo' => $self->payinfo,
1353 'paybatch' => "$processor:". $transaction->authorization,
1355 my $error = $cust_pay->insert;
1357 # gah, even with transactions.
1358 $dbh->commit if $oldAutoCommit; #well.
1359 my $e = 'WARNING: Card debited but database not updated - '.
1360 'error applying payment, invnum #' . $cust_bill->invnum.
1361 " ($processor): $error";
1365 } elsif ( $options{'report_badcard'} ) {
1366 $dbh->commit if $oldAutoCommit;
1367 return "$processor error, invnum #". $cust_bill->invnum. ': '.
1368 $transaction->result_code. ": ". $transaction->error_message;
1370 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1375 $dbh->rollback if $oldAutoCommit;
1376 return "Unknown real-time processor $processor\n";
1379 } else { #batch card
1381 my $cust_pay_batch = new FS::cust_pay_batch ( {
1382 'invnum' => $cust_bill->getfield('invnum'),
1383 'custnum' => $self->getfield('custnum'),
1384 'last' => $self->getfield('last'),
1385 'first' => $self->getfield('first'),
1386 'address1' => $self->getfield('address1'),
1387 'address2' => $self->getfield('address2'),
1388 'city' => $self->getfield('city'),
1389 'state' => $self->getfield('state'),
1390 'zip' => $self->getfield('zip'),
1391 'country' => $self->getfield('country'),
1393 'cardnum' => $self->getfield('payinfo'),
1394 'exp' => $self->getfield('paydate'),
1395 'payname' => $self->getfield('payname'),
1396 'amount' => $amount,
1398 my $error = $cust_pay_batch->insert;
1400 $dbh->rollback if $oldAutoCommit;
1401 return "Error adding to cust_pay_batch: $error";
1407 $dbh->rollback if $oldAutoCommit;
1408 return "Unknown payment type ". $self->payby;
1412 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1419 Returns the total owed for this customer on all invoices
1420 (see L<FS::cust_bill/owed>).
1426 $self->total_owed_date(2145859200); #12/31/2037
1429 =item total_owed_date TIME
1431 Returns the total owed for this customer on all invoices with date earlier than
1432 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
1433 see L<Time::Local> and L<Date::Parse> for conversion functions.
1437 sub total_owed_date {
1441 foreach my $cust_bill (
1442 grep { $_->_date <= $time }
1443 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1445 $total_bill += $cust_bill->owed;
1447 sprintf( "%.2f", $total_bill );
1452 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1453 to outstanding invoice balances in chronological order and returns the value
1454 of any remaining unapplied credits available for refund
1455 (see L<FS::cust_refund>).
1462 return 0 unless $self->total_credited;
1464 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1465 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1467 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1468 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1472 foreach my $cust_bill ( @invoices ) {
1475 if ( !defined($credit) || $credit->credited == 0) {
1476 $credit = pop @credits or last;
1479 if ($cust_bill->owed >= $credit->credited) {
1480 $amount=$credit->credited;
1482 $amount=$cust_bill->owed;
1485 my $cust_credit_bill = new FS::cust_credit_bill ( {
1486 'crednum' => $credit->crednum,
1487 'invnum' => $cust_bill->invnum,
1488 'amount' => $amount,
1490 my $error = $cust_credit_bill->insert;
1491 die $error if $error;
1493 redo if ($cust_bill->owed > 0);
1497 return $self->total_credited;
1500 =item apply_payments
1502 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1503 to outstanding invoice balances in chronological order.
1505 #and returns the value of any remaining unapplied payments.
1509 sub apply_payments {
1514 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1515 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1517 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1518 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1522 foreach my $cust_bill ( @invoices ) {
1525 if ( !defined($payment) || $payment->unapplied == 0 ) {
1526 $payment = pop @payments or last;
1529 if ( $cust_bill->owed >= $payment->unapplied ) {
1530 $amount = $payment->unapplied;
1532 $amount = $cust_bill->owed;
1535 my $cust_bill_pay = new FS::cust_bill_pay ( {
1536 'paynum' => $payment->paynum,
1537 'invnum' => $cust_bill->invnum,
1538 'amount' => $amount,
1540 my $error = $cust_bill_pay->insert;
1541 die $error if $error;
1543 redo if ( $cust_bill->owed > 0);
1547 return $self->total_unapplied_payments;
1550 =item total_credited
1552 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1553 customer. See L<FS::cust_credit/credited>.
1557 sub total_credited {
1559 my $total_credit = 0;
1560 foreach my $cust_credit ( qsearch('cust_credit', {
1561 'custnum' => $self->custnum,
1563 $total_credit += $cust_credit->credited;
1565 sprintf( "%.2f", $total_credit );
1568 =item total_unapplied_payments
1570 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1571 See L<FS::cust_pay/unapplied>.
1575 sub total_unapplied_payments {
1577 my $total_unapplied = 0;
1578 foreach my $cust_pay ( qsearch('cust_pay', {
1579 'custnum' => $self->custnum,
1581 $total_unapplied += $cust_pay->unapplied;
1583 sprintf( "%.2f", $total_unapplied );
1588 Returns the balance for this customer (total_owed minus total_credited
1589 minus total_unapplied_payments).
1596 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1600 =item balance_date TIME
1602 Returns the balance for this customer, only considering invoices with date
1603 earlier than TIME (total_owed_date minus total_credited minus
1604 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
1605 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
1614 $self->total_owed_date($time)
1615 - $self->total_credited
1616 - $self->total_unapplied_payments
1620 =item invoicing_list [ ARRAYREF ]
1622 If an arguement is given, sets these email addresses as invoice recipients
1623 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1624 (except as warnings), so use check_invoicing_list first.
1626 Returns a list of email addresses (with svcnum entries expanded).
1628 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1629 check it without disturbing anything by passing nothing.
1631 This interface may change in the future.
1635 sub invoicing_list {
1636 my( $self, $arrayref ) = @_;
1638 my @cust_main_invoice;
1639 if ( $self->custnum ) {
1640 @cust_main_invoice =
1641 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1643 @cust_main_invoice = ();
1645 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1646 #warn $cust_main_invoice->destnum;
1647 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1648 #warn $cust_main_invoice->destnum;
1649 my $error = $cust_main_invoice->delete;
1650 warn $error if $error;
1653 if ( $self->custnum ) {
1654 @cust_main_invoice =
1655 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1657 @cust_main_invoice = ();
1659 my %seen = map { $_->address => 1 } @cust_main_invoice;
1660 foreach my $address ( @{$arrayref} ) {
1661 #unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1662 next if exists $seen{$address} && $seen{$address};
1663 $seen{$address} = 1;
1664 my $cust_main_invoice = new FS::cust_main_invoice ( {
1665 'custnum' => $self->custnum,
1668 my $error = $cust_main_invoice->insert;
1669 warn $error if $error;
1672 if ( $self->custnum ) {
1674 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1680 =item check_invoicing_list ARRAYREF
1682 Checks these arguements as valid input for the invoicing_list method. If there
1683 is an error, returns the error, otherwise returns false.
1687 sub check_invoicing_list {
1688 my( $self, $arrayref ) = @_;
1689 foreach my $address ( @{$arrayref} ) {
1690 my $cust_main_invoice = new FS::cust_main_invoice ( {
1691 'custnum' => $self->custnum,
1694 my $error = $self->custnum
1695 ? $cust_main_invoice->check
1696 : $cust_main_invoice->checkdest
1698 return $error if $error;
1703 =item default_invoicing_list
1705 Returns the email addresses of any
1709 sub default_invoicing_list {
1712 foreach my $cust_pkg ( $self->all_pkgs ) {
1713 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1715 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1716 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1718 push @list, map { $_->email } @svc_acct;
1720 $self->invoicing_list(\@list);
1723 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1725 Returns an array of customers referred by this customer (referral_custnum set
1726 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1727 customers referred by customers referred by this customer and so on, inclusive.
1728 The default behavior is DEPTH 1 (no recursion).
1732 sub referral_cust_main {
1734 my $depth = @_ ? shift : 1;
1735 my $exclude = @_ ? shift : {};
1738 map { $exclude->{$_->custnum}++; $_; }
1739 grep { ! $exclude->{ $_->custnum } }
1740 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1744 map { $_->referral_cust_main($depth-1, $exclude) }
1751 =item referral_cust_pkg [ DEPTH ]
1753 Like referral_cust_main, except returns a flat list of all unsuspended packages
1754 for each customer. The number of items in this list may be useful for
1755 comission calculations (perhaps after a grep).
1759 sub referral_cust_pkg {
1761 my $depth = @_ ? shift : 1;
1763 map { $_->unsuspended_pkgs }
1764 grep { $_->unsuspended_pkgs }
1765 $self->referral_cust_main($depth);
1768 =item credit AMOUNT, REASON
1770 Applies a credit to this customer. If there is an error, returns the error,
1771 otherwise returns false.
1776 my( $self, $amount, $reason ) = @_;
1777 my $cust_credit = new FS::cust_credit {
1778 'custnum' => $self->custnum,
1779 'amount' => $amount,
1780 'reason' => $reason,
1782 $cust_credit->insert;
1785 =item charge AMOUNT PKG COMMENT
1787 Creates a one-time charge for this customer. If there is an error, returns
1788 the error, otherwise returns false.
1793 my ( $self, $amount, $pkg, $comment ) = @_;
1795 my $part_pkg = new FS::part_pkg ( {
1796 'pkg' => $pkg || 'One-time charge',
1797 'comment' => $comment,
1814 =item check_and_rebuild_fuzzyfiles
1818 sub check_and_rebuild_fuzzyfiles {
1819 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1820 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1821 or &rebuild_fuzzyfiles;
1824 =item rebuild_fuzzyfiles
1828 sub rebuild_fuzzyfiles {
1830 use Fcntl qw(:flock);
1832 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1836 open(LASTLOCK,">>$dir/cust_main.last")
1837 or die "can't open $dir/cust_main.last: $!";
1838 flock(LASTLOCK,LOCK_EX)
1839 or die "can't lock $dir/cust_main.last: $!";
1841 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1843 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1844 if defined dbdef->table('cust_main')->column('ship_last');
1846 open (LASTCACHE,">$dir/cust_main.last.tmp")
1847 or die "can't open $dir/cust_main.last.tmp: $!";
1848 print LASTCACHE join("\n", @all_last), "\n";
1849 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1851 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1856 open(COMPANYLOCK,">>$dir/cust_main.company")
1857 or die "can't open $dir/cust_main.company: $!";
1858 flock(COMPANYLOCK,LOCK_EX)
1859 or die "can't lock $dir/cust_main.company: $!";
1861 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1863 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1864 if defined dbdef->table('cust_main')->column('ship_last');
1866 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1867 or die "can't open $dir/cust_main.company.tmp: $!";
1868 print COMPANYCACHE join("\n", @all_company), "\n";
1869 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1871 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1881 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1882 open(LASTCACHE,"<$dir/cust_main.last")
1883 or die "can't open $dir/cust_main.last: $!";
1884 my @array = map { chomp; $_; } <LASTCACHE>;
1894 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1895 open(COMPANYCACHE,"<$dir/cust_main.company")
1896 or die "can't open $dir/cust_main.last: $!";
1897 my @array = map { chomp; $_; } <COMPANYCACHE>;
1902 =item append_fuzzyfiles LASTNAME COMPANY
1906 sub append_fuzzyfiles {
1907 my( $last, $company ) = @_;
1909 &check_and_rebuild_fuzzyfiles;
1911 use Fcntl qw(:flock);
1913 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1917 open(LAST,">>$dir/cust_main.last")
1918 or die "can't open $dir/cust_main.last: $!";
1920 or die "can't lock $dir/cust_main.last: $!";
1922 print LAST "$last\n";
1925 or die "can't unlock $dir/cust_main.last: $!";
1931 open(COMPANY,">>$dir/cust_main.company")
1932 or die "can't open $dir/cust_main.company: $!";
1933 flock(COMPANY,LOCK_EX)
1934 or die "can't lock $dir/cust_main.company: $!";
1936 print COMPANY "$company\n";
1938 flock(COMPANY,LOCK_UN)
1939 or die "can't unlock $dir/cust_main.company: $!";
1949 $Id: cust_main.pm,v 1.53 2001-12-28 15:14:01 ivan Exp $
1955 The delete method should possibly take an FS::cust_main object reference
1956 instead of a scalar customer number.
1958 Bill and collect options should probably be passed as references instead of a
1961 CyberCash v2 forces us to define some variables in package main.
1963 There should probably be a configuration file with a list of allowed credit
1966 No multiple currency support (probably a larger project than just this module).
1970 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1971 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1972 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1973 L<FS::UID>, schema.html from the base documentation.