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);
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;
32 @ISA = qw( FS::Record );
37 #ask FS::UID to run this stuff for us later
38 $FS::UID::callback{'FS::cust_main'} = sub {
40 $lpr = $conf->config('lpr');
41 $invoice_from = $conf->config('invoice_from');
42 $smtpmachine = $conf->config('smtpmachine');
44 if ( $conf->exists('cybercash3.2') ) {
46 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
47 require CCMckDirectLib3_2;
49 require CCMckErrno3_2;
50 #qw(MCKGetErrorMessage $E_NoErr);
51 import CCMckErrno3_2 qw($E_NoErr);
54 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
55 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
56 if ( $status != $E_NoErr ) {
57 warn "CCMckLib3_2::InitConfig error:\n";
58 foreach my $key (keys %CCMckLib3_2::Config) {
59 warn " $key => $CCMckLib3_2::Config{$key}\n"
61 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
62 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
64 $processor='cybercash3.2';
65 } elsif ( $conf->exists('business-onlinepayment') ) {
71 ) = $conf->config('business-onlinepayment');
72 $bop_action ||= 'normal authorization';
73 eval "use Business::OnlinePayment";
74 $processor="Business::OnlinePayment::$bop_processor";
80 FS::cust_main - Object methods for cust_main records
86 $record = new FS::cust_main \%hash;
87 $record = new FS::cust_main { 'column' => 'value' };
89 $error = $record->insert;
91 $error = $new_record->replace($old_record);
93 $error = $record->delete;
95 $error = $record->check;
97 @cust_pkg = $record->all_pkgs;
99 @cust_pkg = $record->ncancelled_pkgs;
101 $error = $record->bill;
102 $error = $record->bill %options;
103 $error = $record->bill 'time' => $time;
105 $error = $record->collect;
106 $error = $record->collect %options;
107 $error = $record->collect 'invoice_time' => $time,
108 'batch_card' => 'yes',
109 'report_badcard' => 'yes',
114 An FS::cust_main object represents a customer. FS::cust_main inherits from
115 FS::Record. The following fields are currently supported:
119 =item custnum - primary key (assigned automatically for new customers)
121 =item agentnum - agent (see L<FS::agent>)
123 =item refnum - referral (see L<FS::part_referral>)
129 =item ss - social security number (optional)
131 =item company - (optional)
135 =item address2 - (optional)
139 =item county - (optional, see L<FS::cust_main_county>)
141 =item state - (see L<FS::cust_main_county>)
145 =item country - (see L<FS::cust_main_county>)
147 =item daytime - phone (optional)
149 =item night - phone (optional)
151 =item fax - phone (optional)
153 =item ship_first - name
155 =item ship_last - name
157 =item ship_company - (optional)
161 =item ship_address2 - (optional)
165 =item ship_county - (optional, see L<FS::cust_main_county>)
167 =item ship_state - (see L<FS::cust_main_county>)
171 =item ship_country - (see L<FS::cust_main_county>)
173 =item ship_daytime - phone (optional)
175 =item ship_night - phone (optional)
177 =item ship_fax - phone (optional)
179 =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)
181 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
183 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
185 =item payname - name on card or billing name
187 =item tax - tax exempt, empty or `Y'
189 =item otaker - order taker (assigned automatically, see L<FS::UID>)
191 =item comments - comments (optional)
201 Creates a new customer. To add the customer to the database, see L<"insert">.
203 Note that this stores the hash reference, not a distinct copy of the hash it
204 points to. You can ask the object for a copy with the I<hash> method.
208 sub table { 'cust_main'; }
210 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
212 Adds this customer to the database. If there is an error, returns the error,
213 otherwise returns false.
215 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
216 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
217 are inserted atomicly, or the transaction is rolled back (this requries a
218 transactional database). Passing an empty hash reference is equivalent to
219 not supplying this parameter. There should be a better explanation of this,
220 but until then, here's an example:
223 tie %hash, 'Tie::RefHash'; #this part is important
225 $cust_pkg => [ $svc_acct ],
228 $cust_main->insert( \%hash );
230 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
231 be set as the invoicing list (see L<"invoicing_list">). Errors return as
232 expected and rollback the entire transaction; it is not necessary to call
233 check_invoicing_list first. The invoicing_list is set after the records in the
234 CUST_PKG_HASHREF above are inserted, so it is now possible set set an
235 invoicing_list destination to the newly-created svc_acct. Here's an example:
237 $cust_main->insert( {}, [ $email, 'POST' ] );
245 local $SIG{HUP} = 'IGNORE';
246 local $SIG{INT} = 'IGNORE';
247 local $SIG{QUIT} = 'IGNORE';
248 local $SIG{TERM} = 'IGNORE';
249 local $SIG{TSTP} = 'IGNORE';
250 local $SIG{PIPE} = 'IGNORE';
252 my $oldAutoCommit = $FS::UID::AutoCommit;
253 local $FS::UID::AutoCommit = 0;
258 if ( $self->payby eq 'PREPAY' ) {
259 $self->payby('BILL');
260 my $prepay_credit = qsearchs(
262 { 'identifier' => $self->payinfo },
266 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
267 unless $prepay_credit;
268 $amount = $prepay_credit->amount;
269 $seconds = $prepay_credit->seconds;
270 my $error = $prepay_credit->delete;
272 $dbh->rollback if $oldAutoCommit;
273 return "removing prepay_credit (transaction rolled back): $error";
277 my $error = $self->SUPER::insert;
279 $dbh->rollback if $oldAutoCommit;
280 return "inserting cust_main record (transaction rolled back): $error";
283 if ( @param ) { # CUST_PKG_HASHREF
284 my $cust_pkgs = shift @param;
285 foreach my $cust_pkg ( keys %$cust_pkgs ) {
286 $cust_pkg->custnum( $self->custnum );
287 $error = $cust_pkg->insert;
289 $dbh->rollback if $oldAutoCommit;
290 return "inserting cust_pkg (transaction rolled back): $error";
292 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
293 $svc_something->pkgnum( $cust_pkg->pkgnum );
294 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
295 $svc_something->seconds( $svc_something->seconds + $seconds );
298 $error = $svc_something->insert;
300 $dbh->rollback if $oldAutoCommit;
301 return "inserting svc_ (transaction rolled back): $error";
308 $dbh->rollback if $oldAutoCommit;
309 return "No svc_acct record to apply pre-paid time";
312 if ( @param ) { # INVOICING_LIST_ARYREF
313 my $invoicing_list = shift @param;
314 $error = $self->check_invoicing_list( $invoicing_list );
316 $dbh->rollback if $oldAutoCommit;
317 return "checking invoicing_list (transaction rolled back): $error";
319 $self->invoicing_list( $invoicing_list );
323 my $cust_credit = new FS::cust_credit {
324 'custnum' => $self->custnum,
327 $error = $cust_credit->insert;
329 $dbh->rollback if $oldAutoCommit;
330 return "inserting credit (transaction rolled back): $error";
334 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
335 $error = $queue->insert($self->getfield('last'), $self->company);
337 $dbh->rollback if $oldAutoCommit;
338 return "queueing job (transaction rolled back): $error";
341 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
342 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
343 $error = $queue->insert($self->getfield('last'), $self->company);
345 $dbh->rollback if $oldAutoCommit;
346 return "queueing job (transaction rolled back): $error";
350 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
355 =item delete NEW_CUSTNUM
357 This deletes the customer. If there is an error, returns the error, otherwise
360 This will completely remove all traces of the customer record. This is not
361 what you want when a customer cancels service; for that, cancel all of the
362 customer's packages (see L<FS::cust_pkg/cancel>).
364 If the customer has any packages, you need to pass a new (valid) customer
365 number for those packages to be transferred to.
367 You can't delete a customer with invoices (see L<FS::cust_bill>),
368 or credits (see L<FS::cust_credit>).
375 local $SIG{HUP} = 'IGNORE';
376 local $SIG{INT} = 'IGNORE';
377 local $SIG{QUIT} = 'IGNORE';
378 local $SIG{TERM} = 'IGNORE';
379 local $SIG{TSTP} = 'IGNORE';
380 local $SIG{PIPE} = 'IGNORE';
382 my $oldAutoCommit = $FS::UID::AutoCommit;
383 local $FS::UID::AutoCommit = 0;
386 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
387 $dbh->rollback if $oldAutoCommit;
388 return "Can't delete a customer with invoices";
390 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
391 $dbh->rollback if $oldAutoCommit;
392 return "Can't delete a customer with credits";
395 my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
397 my $new_custnum = shift;
398 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
399 $dbh->rollback if $oldAutoCommit;
400 return "Invalid new customer number: $new_custnum";
402 foreach my $cust_pkg ( @cust_pkg ) {
403 my %hash = $cust_pkg->hash;
404 $hash{'custnum'} = $new_custnum;
405 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
406 my $error = $new_cust_pkg->replace($cust_pkg);
408 $dbh->rollback if $oldAutoCommit;
413 foreach my $cust_main_invoice (
414 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
416 my $error = $cust_main_invoice->delete;
418 $dbh->rollback if $oldAutoCommit;
423 my $error = $self->SUPER::delete;
425 $dbh->rollback if $oldAutoCommit;
429 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
434 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
436 Replaces the OLD_RECORD with this one in the database. If there is an error,
437 returns the error, otherwise returns false.
439 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
440 be set as the invoicing list (see L<"invoicing_list">). Errors return as
441 expected and rollback the entire transaction; it is not necessary to call
442 check_invoicing_list first. Here's an example:
444 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
453 local $SIG{HUP} = 'IGNORE';
454 local $SIG{INT} = 'IGNORE';
455 local $SIG{QUIT} = 'IGNORE';
456 local $SIG{TERM} = 'IGNORE';
457 local $SIG{TSTP} = 'IGNORE';
458 local $SIG{PIPE} = 'IGNORE';
460 my $oldAutoCommit = $FS::UID::AutoCommit;
461 local $FS::UID::AutoCommit = 0;
464 my $error = $self->SUPER::replace($old);
467 $dbh->rollback if $oldAutoCommit;
471 if ( @param ) { # INVOICING_LIST_ARYREF
472 my $invoicing_list = shift @param;
473 $error = $self->check_invoicing_list( $invoicing_list );
475 $dbh->rollback if $oldAutoCommit;
478 $self->invoicing_list( $invoicing_list );
481 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
488 Checks all fields to make sure this is a valid customer record. If there is
489 an error, returns the error, otherwise returns false. Called by the insert
498 $self->ut_numbern('custnum')
499 || $self->ut_number('agentnum')
500 || $self->ut_number('refnum')
501 || $self->ut_name('last')
502 || $self->ut_name('first')
503 || $self->ut_textn('company')
504 || $self->ut_text('address1')
505 || $self->ut_textn('address2')
506 || $self->ut_text('city')
507 || $self->ut_textn('county')
508 || $self->ut_textn('state')
509 || $self->ut_country('country')
510 || $self->ut_anything('comments')
511 || $self->ut_numbern('referral_custnum')
513 #barf. need message catalogs. i18n. etc.
514 $error .= "Please select a referral."
515 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
516 return $error if $error;
518 return "Unknown agent"
519 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
521 return "Unknown referral"
522 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
524 return "Unknown referring custnum ". $self->referral_custnum
525 unless ! $self->referral_custnum
526 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
528 if ( $self->ss eq '' ) {
533 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
534 or return "Illegal social security number: ". $self->ss;
535 $self->ss("$1-$2-$3");
538 unless ( qsearchs('cust_main_county', {
539 'country' => $self->country,
542 return "Unknown state/county/country: ".
543 $self->state. "/". $self->county. "/". $self->country
544 unless qsearchs('cust_main_county',{
545 'state' => $self->state,
546 'county' => $self->county,
547 'country' => $self->country,
552 $self->ut_phonen('daytime', $self->country)
553 || $self->ut_phonen('night', $self->country)
554 || $self->ut_phonen('fax', $self->country)
555 || $self->ut_zip('zip', $self->country)
557 return $error if $error;
560 last first company address1 address2 city county state zip
561 country daytime night fax
564 if ( defined $self->dbdef_table->column('ship_last') ) {
565 if ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } @addfields
566 && grep $self->getfield("ship_$_"), grep $_ ne 'state', @addfields
570 $self->ut_name('ship_last')
571 || $self->ut_name('ship_first')
572 || $self->ut_textn('ship_company')
573 || $self->ut_text('ship_address1')
574 || $self->ut_textn('ship_address2')
575 || $self->ut_text('ship_city')
576 || $self->ut_textn('ship_county')
577 || $self->ut_textn('ship_state')
578 || $self->ut_country('ship_country')
580 return $error if $error;
582 #false laziness with above
583 unless ( qsearchs('cust_main_county', {
584 'country' => $self->ship_country,
587 return "Unknown ship_state/ship_county/ship_country: ".
588 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
589 unless qsearchs('cust_main_county',{
590 'state' => $self->ship_state,
591 'county' => $self->ship_county,
592 'country' => $self->ship_country,
598 $self->ut_phonen('ship_daytime', $self->ship_country)
599 || $self->ut_phonen('ship_night', $self->ship_country)
600 || $self->ut_phonen('ship_fax', $self->ship_country)
601 || $self->ut_zip('ship_zip', $self->ship_country)
603 return $error if $error;
605 } else { # ship_ info eq billing info, so don't store dup info in database
606 $self->setfield("ship_$_", '')
607 foreach qw( last first company address1 address2 city county state zip
608 country daytime night fax );
612 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
613 or return "Illegal payby: ". $self->payby;
616 if ( $self->payby eq 'CARD' ) {
618 my $payinfo = $self->payinfo;
620 $payinfo =~ /^(\d{13,16})$/
621 or return "Illegal credit card number: ". $self->payinfo;
623 $self->payinfo($payinfo);
625 or return "Illegal credit card number: ". $self->payinfo;
626 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
628 } elsif ( $self->payby eq 'BILL' ) {
630 $error = $self->ut_textn('payinfo');
631 return "Illegal P.O. number: ". $self->payinfo if $error;
633 } elsif ( $self->payby eq 'COMP' ) {
635 $error = $self->ut_textn('payinfo');
636 return "Illegal comp account issuer: ". $self->payinfo if $error;
638 } elsif ( $self->payby eq 'PREPAY' ) {
640 my $payinfo = $self->payinfo;
641 $payinfo =~ s/\W//g; #anything else would just confuse things
642 $self->payinfo($payinfo);
643 $error = $self->ut_alpha('payinfo');
644 return "Illegal prepayment identifier: ". $self->payinfo if $error;
645 return "Unknown prepayment identifier"
646 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
650 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
651 return "Expriation date required"
652 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
655 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
656 or return "Illegal expiration date: ". $self->paydate;
657 if ( length($2) == 4 ) {
658 $self->paydate("$2-$1-01");
660 $self->paydate("20$2-$1-01");
664 if ( $self->payname eq '' ) {
665 $self->payname( $self->first. " ". $self->getfield('last') );
667 $self->payname =~ /^([\w \,\.\-\']+)$/
668 or return "Illegal billing name: ". $self->payname;
672 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
675 $self->otaker(getotaker);
682 Returns all packages (see L<FS::cust_pkg>) for this customer.
688 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
691 =item ncancelled_pkgs
693 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
697 sub ncancelled_pkgs {
699 @{ [ # force list context
700 qsearch( 'cust_pkg', {
701 'custnum' => $self->custnum,
704 qsearch( 'cust_pkg', {
705 'custnum' => $self->custnum,
713 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
714 conjunction with the collect method.
716 Options are passed as name-value pairs.
718 The only currently available option is `time', which bills the customer as if
719 it were that time. It is specified as a UNIX timestamp; see
720 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
721 functions. For example:
725 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
727 If there is an error, returns the error, otherwise returns false.
732 my( $self, %options ) = @_;
733 my $time = $options{'time'} || time;
738 local $SIG{HUP} = 'IGNORE';
739 local $SIG{INT} = 'IGNORE';
740 local $SIG{QUIT} = 'IGNORE';
741 local $SIG{TERM} = 'IGNORE';
742 local $SIG{TSTP} = 'IGNORE';
743 local $SIG{PIPE} = 'IGNORE';
745 my $oldAutoCommit = $FS::UID::AutoCommit;
746 local $FS::UID::AutoCommit = 0;
749 # find the packages which are due for billing, find out how much they are
750 # & generate invoice database.
752 my( $total_setup, $total_recur ) = ( 0, 0 );
753 my @cust_bill_pkg = ();
755 foreach my $cust_pkg (
756 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
759 next if $cust_pkg->getfield('cancel');
761 #? to avoid use of uninitialized value errors... ?
762 $cust_pkg->setfield('bill', '')
763 unless defined($cust_pkg->bill);
765 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
767 #so we don't modify cust_pkg record unnecessarily
768 my $cust_pkg_mod_flag = 0;
769 my %hash = $cust_pkg->hash;
770 my $old_cust_pkg = new FS::cust_pkg \%hash;
774 unless ( $cust_pkg->setup ) {
775 my $setup_prog = $part_pkg->getfield('setup');
776 $setup_prog =~ /^(.*)$/ or do {
777 $dbh->rollback if $oldAutoCommit;
778 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
784 #$cpt->permit(); #what is necessary?
785 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
786 $setup = $cpt->reval($setup_prog);
787 unless ( defined($setup) ) {
788 $dbh->rollback if $oldAutoCommit;
789 return "Error reval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
792 $cust_pkg->setfield('setup',$time);
793 $cust_pkg_mod_flag=1;
799 if ( $part_pkg->getfield('freq') > 0 &&
800 ! $cust_pkg->getfield('susp') &&
801 ( $cust_pkg->getfield('bill') || 0 ) < $time
803 my $recur_prog = $part_pkg->getfield('recur');
804 $recur_prog =~ /^(.*)$/ or do {
805 $dbh->rollback if $oldAutoCommit;
806 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
812 #$cpt->permit(); #what is necessary?
813 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
814 $recur = $cpt->reval($recur_prog);
815 unless ( defined($recur) ) {
816 $dbh->rollback if $oldAutoCommit;
817 return "Error reval-ing part_pkg->recur pkgpart ".
818 $part_pkg->pkgpart. ": $@";
820 #change this bit to use Date::Manip? CAREFUL with timezones (see
821 # mailing list archive)
822 #$sdate=$cust_pkg->bill || time;
823 #$sdate=$cust_pkg->bill || $time;
824 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
825 my ($sec,$min,$hour,$mday,$mon,$year) =
826 (localtime($sdate) )[0,1,2,3,4,5];
827 $mon += $part_pkg->getfield('freq');
828 until ( $mon < 12 ) { $mon -= 12; $year++; }
829 $cust_pkg->setfield('bill',
830 timelocal($sec,$min,$hour,$mday,$mon,$year));
831 $cust_pkg_mod_flag = 1;
834 warn "\$setup is undefined" unless defined($setup);
835 warn "\$recur is undefined" unless defined($recur);
836 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
838 if ( $cust_pkg_mod_flag ) {
839 $error=$cust_pkg->replace($old_cust_pkg);
840 if ( $error ) { #just in case
841 $dbh->rollback if $oldAutoCommit;
842 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
844 $setup = sprintf( "%.2f", $setup );
845 $recur = sprintf( "%.2f", $recur );
847 $dbh->rollback if $oldAutoCommit;
848 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
851 $dbh->rollback if $oldAutoCommit;
852 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
854 if ( $setup > 0 || $recur > 0 ) {
855 my $cust_bill_pkg = new FS::cust_bill_pkg ({
856 'pkgnum' => $cust_pkg->pkgnum,
860 'edate' => $cust_pkg->bill,
862 push @cust_bill_pkg, $cust_bill_pkg;
863 $total_setup += $setup;
864 $total_recur += $recur;
870 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
872 unless ( @cust_bill_pkg ) {
873 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
877 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
878 my $cust_main_county = qsearchs('cust_main_county',{
879 'state' => $self->state,
880 'county' => $self->county,
881 'country' => $self->country,
883 my $tax = sprintf( "%.2f",
884 $charged * ( $cust_main_county->getfield('tax') / 100 )
886 $charged = sprintf( "%.2f", $charged+$tax );
888 my $cust_bill_pkg = new FS::cust_bill_pkg ({
895 push @cust_bill_pkg, $cust_bill_pkg;
898 my $cust_bill = new FS::cust_bill ( {
899 'custnum' => $self->custnum,
901 'charged' => $charged,
903 $error = $cust_bill->insert;
905 $dbh->rollback if $oldAutoCommit;
906 return "can't create invoice for customer #". $self->custnum. ": $error";
909 my $invnum = $cust_bill->invnum;
911 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
912 warn $cust_bill_pkg->invnum($invnum);
913 $error = $cust_bill_pkg->insert;
915 $dbh->rollback if $oldAutoCommit;
916 return "can't create invoice line item for customer #". $self->custnum.
921 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
925 =item collect OPTIONS
927 (Attempt to) collect money for this customer's outstanding invoices (see
928 L<FS::cust_bill>). Usually used after the bill method.
930 Depending on the value of `payby', this may print an invoice (`BILL'), charge
931 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
933 If there is an error, returns the error, otherwise returns false.
935 Options are passed as name-value pairs.
937 Currently available options are:
939 invoice_time - Use this time when deciding when to print invoices and
940 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>
941 for conversion functions.
943 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
944 default, cards are processed immediately, which will generate an error if
945 CyberCash is not installed.
947 report_badcard - Set this true if you want bad card transactions to
948 return an error. By default, they don't.
953 my( $self, %options ) = @_;
954 my $invoice_time = $options{'invoice_time'} || time;
957 local $SIG{HUP} = 'IGNORE';
958 local $SIG{INT} = 'IGNORE';
959 local $SIG{QUIT} = 'IGNORE';
960 local $SIG{TERM} = 'IGNORE';
961 local $SIG{TSTP} = 'IGNORE';
962 local $SIG{PIPE} = 'IGNORE';
964 my $oldAutoCommit = $FS::UID::AutoCommit;
965 local $FS::UID::AutoCommit = 0;
968 my $balance = $self->balance;
969 warn "collect: balance $balance" if $Debug;
970 unless ( $balance > 0 ) { #redundant?????
971 $dbh->rollback if $oldAutoCommit; #hmm
975 foreach my $cust_bill (
976 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
979 #this has to be before next's
980 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
984 $balance = sprintf( "%.2f", $balance - $amount );
986 next unless $cust_bill->owed > 0;
988 # don't try to charge for the same invoice if it's already in a batch
989 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
991 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
993 next unless $amount > 0;
995 if ( $self->payby eq 'BILL' ) {
998 my $since = $invoice_time - ( $cust_bill->_date || 0 );
999 #warn "$invoice_time ", $cust_bill->_date, " $since";
1000 if ( $since >= 0 #don't print future invoices
1001 && ( $cust_bill->printed * 2592000 ) <= $since
1004 #my @print_text = $cust_bill->print_text; #( date )
1005 my @invoicing_list = $self->invoicing_list;
1006 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
1007 $ENV{SMTPHOSTS} = $smtpmachine;
1008 $ENV{MAILADDRESS} = $invoice_from;
1009 my $header = new Mail::Header ( [
1010 "From: $invoice_from",
1011 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
1012 "Sender: $invoice_from",
1013 "Reply-To: $invoice_from",
1014 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1017 my $message = new Mail::Internet (
1018 'Header' => $header,
1019 'Body' => [ $cust_bill->print_text ], #( date)
1021 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
1023 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
1024 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
1025 print LPR $cust_bill->print_text; #( date )
1027 or die $! ? "Error closing $lpr: $!"
1028 : "Exit status $? from $lpr";
1031 my %hash = $cust_bill->hash;
1033 my $new_cust_bill = new FS::cust_bill(\%hash);
1034 my $error = $new_cust_bill->replace($cust_bill);
1035 warn "Error updating $cust_bill->printed: $error" if $error;
1039 } elsif ( $self->payby eq 'COMP' ) {
1040 my $cust_pay = new FS::cust_pay ( {
1041 'invnum' => $cust_bill->invnum,
1045 'payinfo' => $self->payinfo,
1048 my $error = $cust_pay->insert;
1050 $dbh->rollback if $oldAutoCommit;
1051 return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
1055 } elsif ( $self->payby eq 'CARD' ) {
1057 if ( $options{'batch_card'} ne 'yes' ) {
1059 unless ( $processor ) {
1060 $dbh->rollback if $oldAutoCommit;
1061 return "Real time card processing not enabled!";
1064 my $address = $self->address1;
1065 $address .= ", ". $self->address2 if $self->address2;
1068 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1069 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1072 if ( $processor eq 'cybercash3.2' ) {
1074 #fix exp. date for cybercash
1075 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1076 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1079 my $paybatch = $cust_bill->invnum.
1080 '-' . time2str("%y%m%d%H%M%S", time);
1082 my $payname = $self->payname ||
1083 $self->getfield('first'). ' '. $self->getfield('last');
1086 my $country = $self->country eq 'US' ? 'USA' : $self->country;
1088 my @full_xaction = ( $xaction,
1089 'Order-ID' => $paybatch,
1090 'Amount' => "usd $amount",
1091 'Card-Number' => $self->getfield('payinfo'),
1092 'Card-Name' => $payname,
1093 'Card-Address' => $address,
1094 'Card-City' => $self->getfield('city'),
1095 'Card-State' => $self->getfield('state'),
1096 'Card-Zip' => $self->getfield('zip'),
1097 'Card-Country' => $country,
1102 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1104 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
1105 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
1106 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1107 my $cust_pay = new FS::cust_pay ( {
1108 'invnum' => $cust_bill->invnum,
1112 'payinfo' => $self->payinfo,
1113 'paybatch' => "$processor:$paybatch",
1115 my $error = $cust_pay->insert;
1117 # gah, even with transactions.
1118 $dbh->commit if $oldAutoCommit; #well.
1119 my $e = 'WARNING: Card debited but database not updated - '.
1120 'error applying payment, invnum #' . $cust_bill->invnum.
1121 " (CyberCash Order-ID $paybatch): $error";
1125 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1126 || $options{'report_badcard'} ) {
1127 $dbh->commit if $oldAutoCommit;
1128 return 'Cybercash error, invnum #' .
1129 $cust_bill->invnum. ':'. $result{'MErrMsg'};
1131 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1135 } elsif ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
1137 my $bop_processor = $1;
1139 my($payname, $payfirst, $paylast);
1140 if ( $self->payname ) {
1141 $payname = $self->payname;
1142 $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
1144 $dbh->rollback if $oldAutoCommit;
1145 return "Illegal payname $payname";
1147 ($payfirst, $paylast) = ($1, $2);
1149 $payfirst = $self->getfield('first');
1150 $paylast = $self->getfield('first');
1151 $payname = "$payfirst $paylast";
1154 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1155 if ( $conf->exists('emailinvoiceonly') ) {
1156 @invoicing_list = $self->default_invoicing_list
1157 unless @invoicing_list;
1159 my $email = $invoicing_list[0];
1161 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
1164 new Business::OnlinePayment( $bop_processor, @bop_options );
1165 $transaction->content(
1167 'login' => $bop_login,
1168 'password' => $bop_password,
1169 'action' => $action1,
1170 'description' => 'Internet Services',
1171 'amount' => $amount,
1172 'invoice_number' => $cust_bill->invnum,
1173 'customer_id' => $self->custnum,
1174 'last_name' => $paylast,
1175 'first_name' => $payfirst,
1177 'address' => $address,
1178 'city' => $self->city,
1179 'state' => $self->state,
1180 'zip' => $self->zip,
1181 'country' => $self->country,
1182 'card_number' => $self->payinfo,
1183 'expiration' => $exp,
1184 'referer' => 'http://cleanwhisker.420.am/',
1187 $transaction->submit();
1189 if ( $transaction->is_success() && $action2 ) {
1190 my $auth = $transaction->authorization;
1191 my $ordernum = $transaction->order_number;
1192 #warn "********* $auth ***********\n";
1193 #warn "********* $ordernum ***********\n";
1195 new Business::OnlinePayment( $bop_processor, @bop_options );
1199 login => $bop_login,
1200 password => $bop_password,
1201 order_number => $ordernum,
1203 authorization => $auth,
1204 description => 'Internet Services',
1209 unless ( $capture->is_success ) {
1210 my $e = "Authorization sucessful but capture failed, invnum #".
1211 $cust_bill->invnum. ': '. $capture->result_code.
1212 ": ". $capture->error_message;
1219 if ( $transaction->is_success() ) {
1221 my $cust_pay = new FS::cust_pay ( {
1222 'invnum' => $cust_bill->invnum,
1226 'payinfo' => $self->payinfo,
1227 'paybatch' => "$processor:". $transaction->authorization,
1229 my $error = $cust_pay->insert;
1231 # gah, even with transactions.
1232 $dbh->commit if $oldAutoCommit; #well.
1233 my $e = 'WARNING: Card debited but database not updated - '.
1234 'error applying payment, invnum #' . $cust_bill->invnum.
1235 " ($processor): $error";
1239 } elsif ( $options{'report_badcard'} ) {
1240 $dbh->commit if $oldAutoCommit;
1241 return "$processor error, invnum #". $cust_bill->invnum. ': '.
1242 $transaction->result_code. ": ". $transaction->error_message;
1244 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1249 $dbh->rollback if $oldAutoCommit;
1250 return "Unknown real-time processor $processor\n";
1253 } else { #batch card
1255 my $cust_pay_batch = new FS::cust_pay_batch ( {
1256 'invnum' => $cust_bill->getfield('invnum'),
1257 'custnum' => $self->getfield('custnum'),
1258 'last' => $self->getfield('last'),
1259 'first' => $self->getfield('first'),
1260 'address1' => $self->getfield('address1'),
1261 'address2' => $self->getfield('address2'),
1262 'city' => $self->getfield('city'),
1263 'state' => $self->getfield('state'),
1264 'zip' => $self->getfield('zip'),
1265 'country' => $self->getfield('country'),
1267 'cardnum' => $self->getfield('payinfo'),
1268 'exp' => $self->getfield('paydate'),
1269 'payname' => $self->getfield('payname'),
1270 'amount' => $amount,
1272 my $error = $cust_pay_batch->insert;
1274 $dbh->rollback if $oldAutoCommit;
1275 return "Error adding to cust_pay_batch: $error";
1281 $dbh->rollback if $oldAutoCommit;
1282 return "Unknown payment type ". $self->payby;
1286 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1293 Returns the total owed for this customer on all invoices
1294 (see L<FS::cust_bill/owed>).
1301 foreach my $cust_bill ( qsearch('cust_bill', {
1302 'custnum' => $self->custnum,
1304 $total_bill += $cust_bill->owed;
1306 sprintf( "%.2f", $total_bill );
1311 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1312 to outstanding invoice balances in chronological order and returns the value
1313 of any remaining unapplied credits available for refund
1314 (see L<FS::cust_refund>).
1321 return 0 unless $self->total_credited;
1323 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1324 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1326 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1327 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1331 foreach my $cust_bill ( @invoices ) {
1334 if ( !defined($credit) || $credit->credited == 0) {
1335 $credit = pop @credits or last;
1338 if ($cust_bill->owed >= $credit->credited) {
1339 $amount=$credit->credited;
1341 $amount=$cust_bill->owed;
1344 my $cust_credit_bill = new FS::cust_credit_bill ( {
1345 'crednum' => $credit->crednum,
1346 'invnum' => $cust_bill->invnum,
1347 'amount' => $amount,
1349 my $error = $cust_credit_bill->insert;
1350 die $error if $error;
1352 redo if ($cust_bill->owed > 0);
1356 return $self->total_credited;
1359 =item apply_payments
1361 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1362 to outstanding invoice balances in chronological order.
1364 #and returns the value of any remaining unapplied payments.
1368 sub apply_payments {
1373 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1374 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1376 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1377 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1381 foreach my $cust_bill ( @invoices ) {
1384 if ( !defined($payment) || $payment->unapplied == 0 ) {
1385 $payment = pop @payments or last;
1388 if ( $cust_bill->owed >= $payment->unapplied ) {
1389 $amount = $payment->unapplied;
1391 $amount = $cust_bill->owed;
1394 my $cust_bill_pay = new FS::cust_bill_pay ( {
1395 'paynum' => $payment->paynum,
1396 'invnum' => $cust_bill->invnum,
1397 'amount' => $amount,
1399 my $error = $cust_bill_pay->insert;
1400 die $error if $error;
1402 redo if ( $cust_bill->owed > 0);
1409 =item total_credited
1411 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1412 customer. See L<FS::cust_credit/credited>.
1416 sub total_credited {
1418 my $total_credit = 0;
1419 foreach my $cust_credit ( qsearch('cust_credit', {
1420 'custnum' => $self->custnum,
1422 $total_credit += $cust_credit->credited;
1424 sprintf( "%.2f", $total_credit );
1427 =item total_unapplied_payments
1429 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1430 See L<FS::cust_pay/unapplied>.
1434 sub total_unapplied_payments {
1436 my $total_unapplied = 0;
1437 foreach my $cust_pay ( qsearch('cust_pay', {
1438 'custnum' => $self->custnum,
1440 $total_unapplied += $cust_pay->unapplied;
1442 sprintf( "%.2f", $total_unapplied );
1447 Returns the balance for this customer (total_owed minus total_credited
1448 minus total_unapplied_payments).
1455 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1459 =item invoicing_list [ ARRAYREF ]
1461 If an arguement is given, sets these email addresses as invoice recipients
1462 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1463 (except as warnings), so use check_invoicing_list first.
1465 Returns a list of email addresses (with svcnum entries expanded).
1467 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1468 check it without disturbing anything by passing nothing.
1470 This interface may change in the future.
1474 sub invoicing_list {
1475 my( $self, $arrayref ) = @_;
1477 my @cust_main_invoice;
1478 if ( $self->custnum ) {
1479 @cust_main_invoice =
1480 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1482 @cust_main_invoice = ();
1484 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1485 #warn $cust_main_invoice->destnum;
1486 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1487 #warn $cust_main_invoice->destnum;
1488 my $error = $cust_main_invoice->delete;
1489 warn $error if $error;
1492 if ( $self->custnum ) {
1493 @cust_main_invoice =
1494 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1496 @cust_main_invoice = ();
1498 foreach my $address ( @{$arrayref} ) {
1499 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1500 my $cust_main_invoice = new FS::cust_main_invoice ( {
1501 'custnum' => $self->custnum,
1504 my $error = $cust_main_invoice->insert;
1505 warn $error if $error;
1509 if ( $self->custnum ) {
1511 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1517 =item check_invoicing_list ARRAYREF
1519 Checks these arguements as valid input for the invoicing_list method. If there
1520 is an error, returns the error, otherwise returns false.
1524 sub check_invoicing_list {
1525 my( $self, $arrayref ) = @_;
1526 foreach my $address ( @{$arrayref} ) {
1527 my $cust_main_invoice = new FS::cust_main_invoice ( {
1528 'custnum' => $self->custnum,
1531 my $error = $self->custnum
1532 ? $cust_main_invoice->check
1533 : $cust_main_invoice->checkdest
1535 return $error if $error;
1540 =item default_invoicing_list
1544 sub default_invoicing_list {
1547 foreach my $cust_pkg ( $self->all_pkgs ) {
1548 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1550 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1551 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1553 push @list, map { $_->email } @svc_acct;
1555 $self->invoicing_list(\@list);
1558 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1560 Returns an array of customers referred by this customer (referral_custnum set
1561 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1562 customers referred by customers referred by this customer and so on, inclusive.
1563 The default behavior is DEPTH 1 (no recursion).
1567 sub referral_cust_main {
1569 my $depth = @_ ? shift : 1;
1570 my $exclude = @_ ? shift : {};
1573 map { $exclude->{$_->custnum}++; $_; }
1574 grep { ! $exclude->{ $_->custnum } }
1575 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1579 map { $_->referral_cust_main($depth-1, $exclude) }
1592 =item check_and_rebuild_fuzzyfiles
1596 sub check_and_rebuild_fuzzyfiles {
1597 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1598 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1599 or &rebuild_fuzzyfiles;
1602 =item rebuild_fuzzyfiles
1606 sub rebuild_fuzzyfiles {
1608 use Fcntl qw(:flock);
1610 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1614 open(LASTLOCK,">>$dir/cust_main.last")
1615 or die "can't open $dir/cust_main.last: $!";
1616 flock(LASTLOCK,LOCK_EX)
1617 or die "can't lock $dir/cust_main.last: $!";
1619 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1621 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1622 if defined dbdef->table('cust_main')->column('ship_last');
1624 open (LASTCACHE,">$dir/cust_main.last.tmp")
1625 or die "can't open $dir/cust_main.last.tmp: $!";
1626 print LASTCACHE join("\n", @all_last), "\n";
1627 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1629 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1634 open(COMPANYLOCK,">>$dir/cust_main.company")
1635 or die "can't open $dir/cust_main.company: $!";
1636 flock(COMPANYLOCK,LOCK_EX)
1637 or die "can't lock $dir/cust_main.company: $!";
1639 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1641 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1642 if defined dbdef->table('cust_main')->column('ship_last');
1644 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1645 or die "can't open $dir/cust_main.company.tmp: $!";
1646 print COMPANYCACHE join("\n", @all_company), "\n";
1647 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1649 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1659 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1660 open(LASTCACHE,"<$dir/cust_main.last")
1661 or die "can't open $dir/cust_main.last: $!";
1662 my @array = map { chomp; $_; } <LASTCACHE>;
1672 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1673 open(COMPANYCACHE,"<$dir/cust_main.company")
1674 or die "can't open $dir/cust_main.last: $!";
1675 my @array = map { chomp; $_; } <COMPANYCACHE>;
1680 =item append_fuzzyfiles LASTNAME COMPANY
1684 sub append_fuzzyfiles {
1685 my( $last, $company ) = @_;
1687 &check_and_rebuild_fuzzyfiles;
1689 use Fcntl qw(:flock);
1691 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1695 open(LAST,">>$dir/cust_main.last")
1696 or die "can't open $dir/cust_main.last: $!";
1698 or die "can't lock $dir/cust_main.last: $!";
1700 print LAST "$last\n";
1703 or die "can't unlock $dir/cust_main.last: $!";
1709 open(COMPANY,">>$dir/cust_main.company")
1710 or die "can't open $dir/cust_main.company: $!";
1711 flock(COMPANY,LOCK_EX)
1712 or die "can't lock $dir/cust_main.company: $!";
1714 print COMPANY "$company\n";
1716 flock(COMPANY,LOCK_UN)
1717 or die "can't unlock $dir/cust_main.company: $!";
1727 $Id: cust_main.pm,v 1.37 2001-09-25 18:03:04 ivan Exp $
1733 The delete method should possibly take an FS::cust_main object reference
1734 instead of a scalar customer number.
1736 Bill and collect options should probably be passed as references instead of a
1739 CyberCash v2 forces us to define some variables in package main.
1741 There should probably be a configuration file with a list of allowed credit
1744 No multiple currency support (probably a larger project than just this module).
1748 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1749 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1750 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1751 L<FS::UID>, schema.html from the base documentation.