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 @cust_pkg = $record->suspended_pkgs;
103 $error = $record->bill;
104 $error = $record->bill %options;
105 $error = $record->bill 'time' => $time;
107 $error = $record->collect;
108 $error = $record->collect %options;
109 $error = $record->collect 'invoice_time' => $time,
110 'batch_card' => 'yes',
111 'report_badcard' => 'yes',
116 An FS::cust_main object represents a customer. FS::cust_main inherits from
117 FS::Record. The following fields are currently supported:
121 =item custnum - primary key (assigned automatically for new customers)
123 =item agentnum - agent (see L<FS::agent>)
125 =item refnum - referral (see L<FS::part_referral>)
131 =item ss - social security number (optional)
133 =item company - (optional)
137 =item address2 - (optional)
141 =item county - (optional, see L<FS::cust_main_county>)
143 =item state - (see L<FS::cust_main_county>)
147 =item country - (see L<FS::cust_main_county>)
149 =item daytime - phone (optional)
151 =item night - phone (optional)
153 =item fax - phone (optional)
155 =item ship_first - name
157 =item ship_last - name
159 =item ship_company - (optional)
163 =item ship_address2 - (optional)
167 =item ship_county - (optional, see L<FS::cust_main_county>)
169 =item ship_state - (see L<FS::cust_main_county>)
173 =item ship_country - (see L<FS::cust_main_county>)
175 =item ship_daytime - phone (optional)
177 =item ship_night - phone (optional)
179 =item ship_fax - phone (optional)
181 =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)
183 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
185 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
187 =item payname - name on card or billing name
189 =item tax - tax exempt, empty or `Y'
191 =item otaker - order taker (assigned automatically, see L<FS::UID>)
193 =item comments - comments (optional)
203 Creates a new customer. To add the customer to the database, see L<"insert">.
205 Note that this stores the hash reference, not a distinct copy of the hash it
206 points to. You can ask the object for a copy with the I<hash> method.
210 sub table { 'cust_main'; }
212 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
214 Adds this customer to the database. If there is an error, returns the error,
215 otherwise returns false.
217 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
218 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
219 are inserted atomicly, or the transaction is rolled back (this requries a
220 transactional database). Passing an empty hash reference is equivalent to
221 not supplying this parameter. There should be a better explanation of this,
222 but until then, here's an example:
225 tie %hash, 'Tie::RefHash'; #this part is important
227 $cust_pkg => [ $svc_acct ],
230 $cust_main->insert( \%hash );
232 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
233 be set as the invoicing list (see L<"invoicing_list">). Errors return as
234 expected and rollback the entire transaction; it is not necessary to call
235 check_invoicing_list first. The invoicing_list is set after the records in the
236 CUST_PKG_HASHREF above are inserted, so it is now possible set set an
237 invoicing_list destination to the newly-created svc_acct. Here's an example:
239 $cust_main->insert( {}, [ $email, 'POST' ] );
247 local $SIG{HUP} = 'IGNORE';
248 local $SIG{INT} = 'IGNORE';
249 local $SIG{QUIT} = 'IGNORE';
250 local $SIG{TERM} = 'IGNORE';
251 local $SIG{TSTP} = 'IGNORE';
252 local $SIG{PIPE} = 'IGNORE';
254 my $oldAutoCommit = $FS::UID::AutoCommit;
255 local $FS::UID::AutoCommit = 0;
260 if ( $self->payby eq 'PREPAY' ) {
261 $self->payby('BILL');
262 my $prepay_credit = qsearchs(
264 { 'identifier' => $self->payinfo },
268 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
269 unless $prepay_credit;
270 $amount = $prepay_credit->amount;
271 $seconds = $prepay_credit->seconds;
272 my $error = $prepay_credit->delete;
274 $dbh->rollback if $oldAutoCommit;
275 return "removing prepay_credit (transaction rolled back): $error";
279 my $error = $self->SUPER::insert;
281 $dbh->rollback if $oldAutoCommit;
282 return "inserting cust_main record (transaction rolled back): $error";
285 if ( @param ) { # CUST_PKG_HASHREF
286 my $cust_pkgs = shift @param;
287 foreach my $cust_pkg ( keys %$cust_pkgs ) {
288 $cust_pkg->custnum( $self->custnum );
289 $error = $cust_pkg->insert;
291 $dbh->rollback if $oldAutoCommit;
292 return "inserting cust_pkg (transaction rolled back): $error";
294 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
295 $svc_something->pkgnum( $cust_pkg->pkgnum );
296 if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
297 $svc_something->seconds( $svc_something->seconds + $seconds );
300 $error = $svc_something->insert;
302 $dbh->rollback if $oldAutoCommit;
303 return "inserting svc_ (transaction rolled back): $error";
310 $dbh->rollback if $oldAutoCommit;
311 return "No svc_acct record to apply pre-paid time";
314 if ( @param ) { # INVOICING_LIST_ARYREF
315 my $invoicing_list = shift @param;
316 $error = $self->check_invoicing_list( $invoicing_list );
318 $dbh->rollback if $oldAutoCommit;
319 return "checking invoicing_list (transaction rolled back): $error";
321 $self->invoicing_list( $invoicing_list );
325 my $cust_credit = new FS::cust_credit {
326 'custnum' => $self->custnum,
329 $error = $cust_credit->insert;
331 $dbh->rollback if $oldAutoCommit;
332 return "inserting credit (transaction rolled back): $error";
336 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
337 $error = $queue->insert($self->getfield('last'), $self->company);
339 $dbh->rollback if $oldAutoCommit;
340 return "queueing job (transaction rolled back): $error";
343 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
344 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
345 $error = $queue->insert($self->getfield('last'), $self->company);
347 $dbh->rollback if $oldAutoCommit;
348 return "queueing job (transaction rolled back): $error";
352 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
357 =item delete NEW_CUSTNUM
359 This deletes the customer. If there is an error, returns the error, otherwise
362 This will completely remove all traces of the customer record. This is not
363 what you want when a customer cancels service; for that, cancel all of the
364 customer's packages (see L<FS::cust_pkg/cancel>).
366 If the customer has any packages, you need to pass a new (valid) customer
367 number for those packages to be transferred to.
369 You can't delete a customer with invoices (see L<FS::cust_bill>),
370 or credits (see L<FS::cust_credit>).
377 local $SIG{HUP} = 'IGNORE';
378 local $SIG{INT} = 'IGNORE';
379 local $SIG{QUIT} = 'IGNORE';
380 local $SIG{TERM} = 'IGNORE';
381 local $SIG{TSTP} = 'IGNORE';
382 local $SIG{PIPE} = 'IGNORE';
384 my $oldAutoCommit = $FS::UID::AutoCommit;
385 local $FS::UID::AutoCommit = 0;
388 if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
389 $dbh->rollback if $oldAutoCommit;
390 return "Can't delete a customer with invoices";
392 if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
393 $dbh->rollback if $oldAutoCommit;
394 return "Can't delete a customer with credits";
397 my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
399 my $new_custnum = shift;
400 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
401 $dbh->rollback if $oldAutoCommit;
402 return "Invalid new customer number: $new_custnum";
404 foreach my $cust_pkg ( @cust_pkg ) {
405 my %hash = $cust_pkg->hash;
406 $hash{'custnum'} = $new_custnum;
407 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
408 my $error = $new_cust_pkg->replace($cust_pkg);
410 $dbh->rollback if $oldAutoCommit;
415 foreach my $cust_main_invoice (
416 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
418 my $error = $cust_main_invoice->delete;
420 $dbh->rollback if $oldAutoCommit;
425 my $error = $self->SUPER::delete;
427 $dbh->rollback if $oldAutoCommit;
431 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
436 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
438 Replaces the OLD_RECORD with this one in the database. If there is an error,
439 returns the error, otherwise returns false.
441 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
442 be set as the invoicing list (see L<"invoicing_list">). Errors return as
443 expected and rollback the entire transaction; it is not necessary to call
444 check_invoicing_list first. Here's an example:
446 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
455 local $SIG{HUP} = 'IGNORE';
456 local $SIG{INT} = 'IGNORE';
457 local $SIG{QUIT} = 'IGNORE';
458 local $SIG{TERM} = 'IGNORE';
459 local $SIG{TSTP} = 'IGNORE';
460 local $SIG{PIPE} = 'IGNORE';
462 my $oldAutoCommit = $FS::UID::AutoCommit;
463 local $FS::UID::AutoCommit = 0;
466 my $error = $self->SUPER::replace($old);
469 $dbh->rollback if $oldAutoCommit;
473 if ( @param ) { # INVOICING_LIST_ARYREF
474 my $invoicing_list = shift @param;
475 $error = $self->check_invoicing_list( $invoicing_list );
477 $dbh->rollback if $oldAutoCommit;
480 $self->invoicing_list( $invoicing_list );
483 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
490 Checks all fields to make sure this is a valid customer record. If there is
491 an error, returns the error, otherwise returns false. Called by the insert
500 $self->ut_numbern('custnum')
501 || $self->ut_number('agentnum')
502 || $self->ut_number('refnum')
503 || $self->ut_name('last')
504 || $self->ut_name('first')
505 || $self->ut_textn('company')
506 || $self->ut_text('address1')
507 || $self->ut_textn('address2')
508 || $self->ut_text('city')
509 || $self->ut_textn('county')
510 || $self->ut_textn('state')
511 || $self->ut_country('country')
512 || $self->ut_anything('comments')
513 || $self->ut_numbern('referral_custnum')
515 #barf. need message catalogs. i18n. etc.
516 $error .= "Please select a referral."
517 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
518 return $error if $error;
520 return "Unknown agent"
521 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
523 return "Unknown referral"
524 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
526 return "Unknown referring custnum ". $self->referral_custnum
527 unless ! $self->referral_custnum
528 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
530 if ( $self->ss eq '' ) {
535 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
536 or return "Illegal social security number: ". $self->ss;
537 $self->ss("$1-$2-$3");
540 unless ( qsearchs('cust_main_county', {
541 'country' => $self->country,
544 return "Unknown state/county/country: ".
545 $self->state. "/". $self->county. "/". $self->country
546 unless qsearchs('cust_main_county',{
547 'state' => $self->state,
548 'county' => $self->county,
549 'country' => $self->country,
554 $self->ut_phonen('daytime', $self->country)
555 || $self->ut_phonen('night', $self->country)
556 || $self->ut_phonen('fax', $self->country)
557 || $self->ut_zip('zip', $self->country)
559 return $error if $error;
562 last first company address1 address2 city county state zip
563 country daytime night fax
566 if ( defined $self->dbdef_table->column('ship_last') ) {
567 if ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } @addfields
568 && grep $self->getfield("ship_$_"), grep $_ ne 'state', @addfields
572 $self->ut_name('ship_last')
573 || $self->ut_name('ship_first')
574 || $self->ut_textn('ship_company')
575 || $self->ut_text('ship_address1')
576 || $self->ut_textn('ship_address2')
577 || $self->ut_text('ship_city')
578 || $self->ut_textn('ship_county')
579 || $self->ut_textn('ship_state')
580 || $self->ut_country('ship_country')
582 return $error if $error;
584 #false laziness with above
585 unless ( qsearchs('cust_main_county', {
586 'country' => $self->ship_country,
589 return "Unknown ship_state/ship_county/ship_country: ".
590 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
591 unless qsearchs('cust_main_county',{
592 'state' => $self->ship_state,
593 'county' => $self->ship_county,
594 'country' => $self->ship_country,
600 $self->ut_phonen('ship_daytime', $self->ship_country)
601 || $self->ut_phonen('ship_night', $self->ship_country)
602 || $self->ut_phonen('ship_fax', $self->ship_country)
603 || $self->ut_zip('ship_zip', $self->ship_country)
605 return $error if $error;
607 } else { # ship_ info eq billing info, so don't store dup info in database
608 $self->setfield("ship_$_", '')
609 foreach qw( last first company address1 address2 city county state zip
610 country daytime night fax );
614 $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
615 or return "Illegal payby: ". $self->payby;
618 if ( $self->payby eq 'CARD' ) {
620 my $payinfo = $self->payinfo;
622 $payinfo =~ /^(\d{13,16})$/
623 or return "Illegal credit card number: ". $self->payinfo;
625 $self->payinfo($payinfo);
627 or return "Illegal credit card number: ". $self->payinfo;
628 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
630 } elsif ( $self->payby eq 'BILL' ) {
632 $error = $self->ut_textn('payinfo');
633 return "Illegal P.O. number: ". $self->payinfo if $error;
635 } elsif ( $self->payby eq 'COMP' ) {
637 $error = $self->ut_textn('payinfo');
638 return "Illegal comp account issuer: ". $self->payinfo if $error;
640 } elsif ( $self->payby eq 'PREPAY' ) {
642 my $payinfo = $self->payinfo;
643 $payinfo =~ s/\W//g; #anything else would just confuse things
644 $self->payinfo($payinfo);
645 $error = $self->ut_alpha('payinfo');
646 return "Illegal prepayment identifier: ". $self->payinfo if $error;
647 return "Unknown prepayment identifier"
648 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
652 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
653 return "Expriation date required"
654 unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
657 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
658 or return "Illegal expiration date: ". $self->paydate;
659 if ( length($2) == 4 ) {
660 $self->paydate("$2-$1-01");
662 $self->paydate("20$2-$1-01");
666 if ( $self->payname eq '' ) {
667 $self->payname( $self->first. " ". $self->getfield('last') );
669 $self->payname =~ /^([\w \,\.\-\']+)$/
670 or return "Illegal billing name: ". $self->payname;
674 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
677 $self->otaker(getotaker);
684 Returns all packages (see L<FS::cust_pkg>) for this customer.
690 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
693 =item ncancelled_pkgs
695 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
699 sub ncancelled_pkgs {
701 @{ [ # force list context
702 qsearch( 'cust_pkg', {
703 'custnum' => $self->custnum,
706 qsearch( 'cust_pkg', {
707 'custnum' => $self->custnum,
715 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
721 grep { $_->susp } $self->ncancelled_pkgs;
724 =item unflagged_suspended_pkgs
726 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
727 customer (thouse packages without the `manual_flag' set).
731 sub unflagged_suspended_pkgs {
733 return $self->suspended_pkgs
734 unless dbdef->table('cust_pkg')->column('manual_flag');
735 grep { ! $_->manual_flag } $self->suspended_pkgs;
738 =item unsuspended_pkgs
740 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
745 sub unsuspended_pkgs {
747 grep { ! $_->susp } $self->ncancelled_pkgs;
752 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
753 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
754 on success or a list of errors.
760 grep { $_->unsuspend } $self->suspended_pkgs;
765 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
766 Always returns a list: an empty list on success or a list of errors.
772 grep { $_->suspend } $self->unsuspended_pkgs;
777 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
778 conjunction with the collect method.
780 Options are passed as name-value pairs.
782 The only currently available option is `time', which bills the customer as if
783 it were that time. It is specified as a UNIX timestamp; see
784 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
785 functions. For example:
789 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
791 If there is an error, returns the error, otherwise returns false.
796 my( $self, %options ) = @_;
797 my $time = $options{'time'} || time;
802 local $SIG{HUP} = 'IGNORE';
803 local $SIG{INT} = 'IGNORE';
804 local $SIG{QUIT} = 'IGNORE';
805 local $SIG{TERM} = 'IGNORE';
806 local $SIG{TSTP} = 'IGNORE';
807 local $SIG{PIPE} = 'IGNORE';
809 my $oldAutoCommit = $FS::UID::AutoCommit;
810 local $FS::UID::AutoCommit = 0;
813 # find the packages which are due for billing, find out how much they are
814 # & generate invoice database.
816 my( $total_setup, $total_recur ) = ( 0, 0 );
817 my @cust_bill_pkg = ();
819 foreach my $cust_pkg (
820 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
823 next if $cust_pkg->getfield('cancel');
825 #? to avoid use of uninitialized value errors... ?
826 $cust_pkg->setfield('bill', '')
827 unless defined($cust_pkg->bill);
829 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
831 #so we don't modify cust_pkg record unnecessarily
832 my $cust_pkg_mod_flag = 0;
833 my %hash = $cust_pkg->hash;
834 my $old_cust_pkg = new FS::cust_pkg \%hash;
838 unless ( $cust_pkg->setup ) {
839 my $setup_prog = $part_pkg->getfield('setup');
840 $setup_prog =~ /^(.*)$/ or do {
841 $dbh->rollback if $oldAutoCommit;
842 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
848 #$cpt->permit(); #what is necessary?
849 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
850 $setup = $cpt->reval($setup_prog);
851 unless ( defined($setup) ) {
852 $dbh->rollback if $oldAutoCommit;
853 return "Error reval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
856 $cust_pkg->setfield('setup',$time);
857 $cust_pkg_mod_flag=1;
863 if ( $part_pkg->getfield('freq') > 0 &&
864 ! $cust_pkg->getfield('susp') &&
865 ( $cust_pkg->getfield('bill') || 0 ) < $time
867 my $recur_prog = $part_pkg->getfield('recur');
868 $recur_prog =~ /^(.*)$/ or do {
869 $dbh->rollback if $oldAutoCommit;
870 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
876 #$cpt->permit(); #what is necessary?
877 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
878 $recur = $cpt->reval($recur_prog);
879 unless ( defined($recur) ) {
880 $dbh->rollback if $oldAutoCommit;
881 return "Error reval-ing part_pkg->recur pkgpart ".
882 $part_pkg->pkgpart. ": $@";
884 #change this bit to use Date::Manip? CAREFUL with timezones (see
885 # mailing list archive)
886 #$sdate=$cust_pkg->bill || time;
887 #$sdate=$cust_pkg->bill || $time;
888 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
889 my ($sec,$min,$hour,$mday,$mon,$year) =
890 (localtime($sdate) )[0,1,2,3,4,5];
891 $mon += $part_pkg->getfield('freq');
892 until ( $mon < 12 ) { $mon -= 12; $year++; }
893 $cust_pkg->setfield('bill',
894 timelocal($sec,$min,$hour,$mday,$mon,$year));
895 $cust_pkg_mod_flag = 1;
898 warn "\$setup is undefined" unless defined($setup);
899 warn "\$recur is undefined" unless defined($recur);
900 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
902 if ( $cust_pkg_mod_flag ) {
903 $error=$cust_pkg->replace($old_cust_pkg);
904 if ( $error ) { #just in case
905 $dbh->rollback if $oldAutoCommit;
906 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
908 $setup = sprintf( "%.2f", $setup );
909 $recur = sprintf( "%.2f", $recur );
911 $dbh->rollback if $oldAutoCommit;
912 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
915 $dbh->rollback if $oldAutoCommit;
916 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
918 if ( $setup > 0 || $recur > 0 ) {
919 my $cust_bill_pkg = new FS::cust_bill_pkg ({
920 'pkgnum' => $cust_pkg->pkgnum,
924 'edate' => $cust_pkg->bill,
926 push @cust_bill_pkg, $cust_bill_pkg;
927 $total_setup += $setup;
928 $total_recur += $recur;
934 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
936 unless ( @cust_bill_pkg ) {
937 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
941 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
942 my $cust_main_county = qsearchs('cust_main_county',{
943 'state' => $self->state,
944 'county' => $self->county,
945 'country' => $self->country,
947 my $tax = sprintf( "%.2f",
948 $charged * ( $cust_main_county->getfield('tax') / 100 )
950 $charged = sprintf( "%.2f", $charged+$tax );
952 my $cust_bill_pkg = new FS::cust_bill_pkg ({
959 push @cust_bill_pkg, $cust_bill_pkg;
962 my $cust_bill = new FS::cust_bill ( {
963 'custnum' => $self->custnum,
965 'charged' => $charged,
967 $error = $cust_bill->insert;
969 $dbh->rollback if $oldAutoCommit;
970 return "can't create invoice for customer #". $self->custnum. ": $error";
973 my $invnum = $cust_bill->invnum;
975 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
976 warn $cust_bill_pkg->invnum($invnum);
977 $error = $cust_bill_pkg->insert;
979 $dbh->rollback if $oldAutoCommit;
980 return "can't create invoice line item for customer #". $self->custnum.
985 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
989 =item collect OPTIONS
991 (Attempt to) collect money for this customer's outstanding invoices (see
992 L<FS::cust_bill>). Usually used after the bill method.
994 Depending on the value of `payby', this may print an invoice (`BILL'), charge
995 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
997 If there is an error, returns the error, otherwise returns false.
999 Options are passed as name-value pairs.
1001 Currently available options are:
1003 invoice_time - Use this time when deciding when to print invoices and
1004 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>
1005 for conversion functions.
1007 batch_card - Set this true to batch cards (see L<FS::cust_pay_batch>). By
1008 default, cards are processed immediately, which will generate an error if
1009 CyberCash is not installed.
1011 report_badcard - Set this true if you want bad card transactions to
1012 return an error. By default, they don't.
1017 my( $self, %options ) = @_;
1018 my $invoice_time = $options{'invoice_time'} || time;
1021 local $SIG{HUP} = 'IGNORE';
1022 local $SIG{INT} = 'IGNORE';
1023 local $SIG{QUIT} = 'IGNORE';
1024 local $SIG{TERM} = 'IGNORE';
1025 local $SIG{TSTP} = 'IGNORE';
1026 local $SIG{PIPE} = 'IGNORE';
1028 my $oldAutoCommit = $FS::UID::AutoCommit;
1029 local $FS::UID::AutoCommit = 0;
1032 my $balance = $self->balance;
1033 warn "collect: balance $balance" if $Debug;
1034 unless ( $balance > 0 ) { #redundant?????
1035 $dbh->rollback if $oldAutoCommit; #hmm
1039 foreach my $cust_bill (
1040 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1043 #this has to be before next's
1044 my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1048 $balance = sprintf( "%.2f", $balance - $amount );
1050 next unless $cust_bill->owed > 0;
1052 # don't try to charge for the same invoice if it's already in a batch
1053 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1055 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1057 next unless $amount > 0;
1059 if ( $self->payby eq 'BILL' ) {
1062 my $since = $invoice_time - ( $cust_bill->_date || 0 );
1063 #warn "$invoice_time ", $cust_bill->_date, " $since";
1064 if ( $since >= 0 #don't print future invoices
1065 && ( $cust_bill->printed * 2592000 ) <= $since
1068 #my @print_text = $cust_bill->print_text; #( date )
1069 my @invoicing_list = $self->invoicing_list;
1070 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
1071 $ENV{SMTPHOSTS} = $smtpmachine;
1072 $ENV{MAILADDRESS} = $invoice_from;
1073 my $header = new Mail::Header ( [
1074 "From: $invoice_from",
1075 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
1076 "Sender: $invoice_from",
1077 "Reply-To: $invoice_from",
1078 "Date: ". time2str("%a, %d %b %Y %X %z", time),
1081 my $message = new Mail::Internet (
1082 'Header' => $header,
1083 'Body' => [ $cust_bill->print_text ], #( date)
1085 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
1087 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
1088 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
1089 print LPR $cust_bill->print_text; #( date )
1091 or die $! ? "Error closing $lpr: $!"
1092 : "Exit status $? from $lpr";
1095 my %hash = $cust_bill->hash;
1097 my $new_cust_bill = new FS::cust_bill(\%hash);
1098 my $error = $new_cust_bill->replace($cust_bill);
1099 warn "Error updating $cust_bill->printed: $error" if $error;
1103 } elsif ( $self->payby eq 'COMP' ) {
1104 my $cust_pay = new FS::cust_pay ( {
1105 'invnum' => $cust_bill->invnum,
1109 'payinfo' => $self->payinfo,
1112 my $error = $cust_pay->insert;
1114 $dbh->rollback if $oldAutoCommit;
1115 return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
1119 } elsif ( $self->payby eq 'CARD' ) {
1121 if ( $options{'batch_card'} ne 'yes' ) {
1123 unless ( $processor ) {
1124 $dbh->rollback if $oldAutoCommit;
1125 return "Real time card processing not enabled!";
1128 my $address = $self->address1;
1129 $address .= ", ". $self->address2 if $self->address2;
1132 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1133 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1136 if ( $processor eq 'cybercash3.2' ) {
1138 #fix exp. date for cybercash
1139 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1140 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1143 my $paybatch = $cust_bill->invnum.
1144 '-' . time2str("%y%m%d%H%M%S", time);
1146 my $payname = $self->payname ||
1147 $self->getfield('first'). ' '. $self->getfield('last');
1150 my $country = $self->country eq 'US' ? 'USA' : $self->country;
1152 my @full_xaction = ( $xaction,
1153 'Order-ID' => $paybatch,
1154 'Amount' => "usd $amount",
1155 'Card-Number' => $self->getfield('payinfo'),
1156 'Card-Name' => $payname,
1157 'Card-Address' => $address,
1158 'Card-City' => $self->getfield('city'),
1159 'Card-State' => $self->getfield('state'),
1160 'Card-Zip' => $self->getfield('zip'),
1161 'Card-Country' => $country,
1166 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1168 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
1169 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
1170 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1171 my $cust_pay = new FS::cust_pay ( {
1172 'invnum' => $cust_bill->invnum,
1176 'payinfo' => $self->payinfo,
1177 'paybatch' => "$processor:$paybatch",
1179 my $error = $cust_pay->insert;
1181 # gah, even with transactions.
1182 $dbh->commit if $oldAutoCommit; #well.
1183 my $e = 'WARNING: Card debited but database not updated - '.
1184 'error applying payment, invnum #' . $cust_bill->invnum.
1185 " (CyberCash Order-ID $paybatch): $error";
1189 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1190 || $options{'report_badcard'} ) {
1191 $dbh->commit if $oldAutoCommit;
1192 return 'Cybercash error, invnum #' .
1193 $cust_bill->invnum. ':'. $result{'MErrMsg'};
1195 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1199 } elsif ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
1201 my $bop_processor = $1;
1203 my($payname, $payfirst, $paylast);
1204 if ( $self->payname ) {
1205 $payname = $self->payname;
1206 $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
1208 $dbh->rollback if $oldAutoCommit;
1209 return "Illegal payname $payname";
1211 ($payfirst, $paylast) = ($1, $2);
1213 $payfirst = $self->getfield('first');
1214 $paylast = $self->getfield('first');
1215 $payname = "$payfirst $paylast";
1218 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1219 if ( $conf->exists('emailinvoiceonly') ) {
1220 @invoicing_list = $self->default_invoicing_list
1221 unless @invoicing_list;
1223 my $email = $invoicing_list[0];
1225 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
1228 new Business::OnlinePayment( $bop_processor, @bop_options );
1229 $transaction->content(
1231 'login' => $bop_login,
1232 'password' => $bop_password,
1233 'action' => $action1,
1234 'description' => 'Internet Services',
1235 'amount' => $amount,
1236 'invoice_number' => $cust_bill->invnum,
1237 'customer_id' => $self->custnum,
1238 'last_name' => $paylast,
1239 'first_name' => $payfirst,
1241 'address' => $address,
1242 'city' => $self->city,
1243 'state' => $self->state,
1244 'zip' => $self->zip,
1245 'country' => $self->country,
1246 'card_number' => $self->payinfo,
1247 'expiration' => $exp,
1248 'referer' => 'http://cleanwhisker.420.am/',
1251 $transaction->submit();
1253 if ( $transaction->is_success() && $action2 ) {
1254 my $auth = $transaction->authorization;
1255 my $ordernum = $transaction->order_number;
1256 #warn "********* $auth ***********\n";
1257 #warn "********* $ordernum ***********\n";
1259 new Business::OnlinePayment( $bop_processor, @bop_options );
1263 login => $bop_login,
1264 password => $bop_password,
1265 order_number => $ordernum,
1267 authorization => $auth,
1268 description => 'Internet Services',
1273 unless ( $capture->is_success ) {
1274 my $e = "Authorization sucessful but capture failed, invnum #".
1275 $cust_bill->invnum. ': '. $capture->result_code.
1276 ": ". $capture->error_message;
1283 if ( $transaction->is_success() ) {
1285 my $cust_pay = new FS::cust_pay ( {
1286 'invnum' => $cust_bill->invnum,
1290 'payinfo' => $self->payinfo,
1291 'paybatch' => "$processor:". $transaction->authorization,
1293 my $error = $cust_pay->insert;
1295 # gah, even with transactions.
1296 $dbh->commit if $oldAutoCommit; #well.
1297 my $e = 'WARNING: Card debited but database not updated - '.
1298 'error applying payment, invnum #' . $cust_bill->invnum.
1299 " ($processor): $error";
1303 } elsif ( $options{'report_badcard'} ) {
1304 $dbh->commit if $oldAutoCommit;
1305 return "$processor error, invnum #". $cust_bill->invnum. ': '.
1306 $transaction->result_code. ": ". $transaction->error_message;
1308 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1313 $dbh->rollback if $oldAutoCommit;
1314 return "Unknown real-time processor $processor\n";
1317 } else { #batch card
1319 my $cust_pay_batch = new FS::cust_pay_batch ( {
1320 'invnum' => $cust_bill->getfield('invnum'),
1321 'custnum' => $self->getfield('custnum'),
1322 'last' => $self->getfield('last'),
1323 'first' => $self->getfield('first'),
1324 'address1' => $self->getfield('address1'),
1325 'address2' => $self->getfield('address2'),
1326 'city' => $self->getfield('city'),
1327 'state' => $self->getfield('state'),
1328 'zip' => $self->getfield('zip'),
1329 'country' => $self->getfield('country'),
1331 'cardnum' => $self->getfield('payinfo'),
1332 'exp' => $self->getfield('paydate'),
1333 'payname' => $self->getfield('payname'),
1334 'amount' => $amount,
1336 my $error = $cust_pay_batch->insert;
1338 $dbh->rollback if $oldAutoCommit;
1339 return "Error adding to cust_pay_batch: $error";
1345 $dbh->rollback if $oldAutoCommit;
1346 return "Unknown payment type ". $self->payby;
1350 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1357 Returns the total owed for this customer on all invoices
1358 (see L<FS::cust_bill/owed>).
1365 foreach my $cust_bill ( qsearch('cust_bill', {
1366 'custnum' => $self->custnum,
1368 $total_bill += $cust_bill->owed;
1370 sprintf( "%.2f", $total_bill );
1375 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1376 to outstanding invoice balances in chronological order and returns the value
1377 of any remaining unapplied credits available for refund
1378 (see L<FS::cust_refund>).
1385 return 0 unless $self->total_credited;
1387 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1388 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1390 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1391 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1395 foreach my $cust_bill ( @invoices ) {
1398 if ( !defined($credit) || $credit->credited == 0) {
1399 $credit = pop @credits or last;
1402 if ($cust_bill->owed >= $credit->credited) {
1403 $amount=$credit->credited;
1405 $amount=$cust_bill->owed;
1408 my $cust_credit_bill = new FS::cust_credit_bill ( {
1409 'crednum' => $credit->crednum,
1410 'invnum' => $cust_bill->invnum,
1411 'amount' => $amount,
1413 my $error = $cust_credit_bill->insert;
1414 die $error if $error;
1416 redo if ($cust_bill->owed > 0);
1420 return $self->total_credited;
1423 =item apply_payments
1425 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1426 to outstanding invoice balances in chronological order.
1428 #and returns the value of any remaining unapplied payments.
1432 sub apply_payments {
1437 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1438 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1440 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1441 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1445 foreach my $cust_bill ( @invoices ) {
1448 if ( !defined($payment) || $payment->unapplied == 0 ) {
1449 $payment = pop @payments or last;
1452 if ( $cust_bill->owed >= $payment->unapplied ) {
1453 $amount = $payment->unapplied;
1455 $amount = $cust_bill->owed;
1458 my $cust_bill_pay = new FS::cust_bill_pay ( {
1459 'paynum' => $payment->paynum,
1460 'invnum' => $cust_bill->invnum,
1461 'amount' => $amount,
1463 my $error = $cust_bill_pay->insert;
1464 die $error if $error;
1466 redo if ( $cust_bill->owed > 0);
1473 =item total_credited
1475 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1476 customer. See L<FS::cust_credit/credited>.
1480 sub total_credited {
1482 my $total_credit = 0;
1483 foreach my $cust_credit ( qsearch('cust_credit', {
1484 'custnum' => $self->custnum,
1486 $total_credit += $cust_credit->credited;
1488 sprintf( "%.2f", $total_credit );
1491 =item total_unapplied_payments
1493 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1494 See L<FS::cust_pay/unapplied>.
1498 sub total_unapplied_payments {
1500 my $total_unapplied = 0;
1501 foreach my $cust_pay ( qsearch('cust_pay', {
1502 'custnum' => $self->custnum,
1504 $total_unapplied += $cust_pay->unapplied;
1506 sprintf( "%.2f", $total_unapplied );
1511 Returns the balance for this customer (total_owed minus total_credited
1512 minus total_unapplied_payments).
1519 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1523 =item invoicing_list [ ARRAYREF ]
1525 If an arguement is given, sets these email addresses as invoice recipients
1526 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
1527 (except as warnings), so use check_invoicing_list first.
1529 Returns a list of email addresses (with svcnum entries expanded).
1531 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
1532 check it without disturbing anything by passing nothing.
1534 This interface may change in the future.
1538 sub invoicing_list {
1539 my( $self, $arrayref ) = @_;
1541 my @cust_main_invoice;
1542 if ( $self->custnum ) {
1543 @cust_main_invoice =
1544 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1546 @cust_main_invoice = ();
1548 foreach my $cust_main_invoice ( @cust_main_invoice ) {
1549 #warn $cust_main_invoice->destnum;
1550 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1551 #warn $cust_main_invoice->destnum;
1552 my $error = $cust_main_invoice->delete;
1553 warn $error if $error;
1556 if ( $self->custnum ) {
1557 @cust_main_invoice =
1558 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1560 @cust_main_invoice = ();
1562 foreach my $address ( @{$arrayref} ) {
1563 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1564 my $cust_main_invoice = new FS::cust_main_invoice ( {
1565 'custnum' => $self->custnum,
1568 my $error = $cust_main_invoice->insert;
1569 warn $error if $error;
1573 if ( $self->custnum ) {
1575 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1581 =item check_invoicing_list ARRAYREF
1583 Checks these arguements as valid input for the invoicing_list method. If there
1584 is an error, returns the error, otherwise returns false.
1588 sub check_invoicing_list {
1589 my( $self, $arrayref ) = @_;
1590 foreach my $address ( @{$arrayref} ) {
1591 my $cust_main_invoice = new FS::cust_main_invoice ( {
1592 'custnum' => $self->custnum,
1595 my $error = $self->custnum
1596 ? $cust_main_invoice->check
1597 : $cust_main_invoice->checkdest
1599 return $error if $error;
1604 =item default_invoicing_list
1608 sub default_invoicing_list {
1611 foreach my $cust_pkg ( $self->all_pkgs ) {
1612 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1614 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1615 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1617 push @list, map { $_->email } @svc_acct;
1619 $self->invoicing_list(\@list);
1622 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1624 Returns an array of customers referred by this customer (referral_custnum set
1625 to this custnum). If DEPTH is given, recurses up to the given depth, returning
1626 customers referred by customers referred by this customer and so on, inclusive.
1627 The default behavior is DEPTH 1 (no recursion).
1631 sub referral_cust_main {
1633 my $depth = @_ ? shift : 1;
1634 my $exclude = @_ ? shift : {};
1637 map { $exclude->{$_->custnum}++; $_; }
1638 grep { ! $exclude->{ $_->custnum } }
1639 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1643 map { $_->referral_cust_main($depth-1, $exclude) }
1656 =item check_and_rebuild_fuzzyfiles
1660 sub check_and_rebuild_fuzzyfiles {
1661 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1662 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1663 or &rebuild_fuzzyfiles;
1666 =item rebuild_fuzzyfiles
1670 sub rebuild_fuzzyfiles {
1672 use Fcntl qw(:flock);
1674 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1678 open(LASTLOCK,">>$dir/cust_main.last")
1679 or die "can't open $dir/cust_main.last: $!";
1680 flock(LASTLOCK,LOCK_EX)
1681 or die "can't lock $dir/cust_main.last: $!";
1683 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1685 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1686 if defined dbdef->table('cust_main')->column('ship_last');
1688 open (LASTCACHE,">$dir/cust_main.last.tmp")
1689 or die "can't open $dir/cust_main.last.tmp: $!";
1690 print LASTCACHE join("\n", @all_last), "\n";
1691 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1693 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1698 open(COMPANYLOCK,">>$dir/cust_main.company")
1699 or die "can't open $dir/cust_main.company: $!";
1700 flock(COMPANYLOCK,LOCK_EX)
1701 or die "can't lock $dir/cust_main.company: $!";
1703 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1705 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1706 if defined dbdef->table('cust_main')->column('ship_last');
1708 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1709 or die "can't open $dir/cust_main.company.tmp: $!";
1710 print COMPANYCACHE join("\n", @all_company), "\n";
1711 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1713 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1723 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1724 open(LASTCACHE,"<$dir/cust_main.last")
1725 or die "can't open $dir/cust_main.last: $!";
1726 my @array = map { chomp; $_; } <LASTCACHE>;
1736 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1737 open(COMPANYCACHE,"<$dir/cust_main.company")
1738 or die "can't open $dir/cust_main.last: $!";
1739 my @array = map { chomp; $_; } <COMPANYCACHE>;
1744 =item append_fuzzyfiles LASTNAME COMPANY
1748 sub append_fuzzyfiles {
1749 my( $last, $company ) = @_;
1751 &check_and_rebuild_fuzzyfiles;
1753 use Fcntl qw(:flock);
1755 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1759 open(LAST,">>$dir/cust_main.last")
1760 or die "can't open $dir/cust_main.last: $!";
1762 or die "can't lock $dir/cust_main.last: $!";
1764 print LAST "$last\n";
1767 or die "can't unlock $dir/cust_main.last: $!";
1773 open(COMPANY,">>$dir/cust_main.company")
1774 or die "can't open $dir/cust_main.company: $!";
1775 flock(COMPANY,LOCK_EX)
1776 or die "can't lock $dir/cust_main.company: $!";
1778 print COMPANY "$company\n";
1780 flock(COMPANY,LOCK_UN)
1781 or die "can't unlock $dir/cust_main.company: $!";
1791 $Id: cust_main.pm,v 1.39 2001-10-09 23:10:16 ivan Exp $
1797 The delete method should possibly take an FS::cust_main object reference
1798 instead of a scalar customer number.
1800 Bill and collect options should probably be passed as references instead of a
1803 CyberCash v2 forces us to define some variables in package main.
1805 There should probably be a configuration file with a list of allowed credit
1808 No multiple currency support (probably a larger project than just this module).
1812 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1813 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1814 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1815 L<FS::UID>, schema.html from the base documentation.