4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
27 use FS::cust_bill_pay;
28 use FS::part_bill_event;
30 @ISA = qw( FS::cust_main_Mixin FS::Record );
34 #ask FS::UID to run this stuff for us later
35 FS::UID->install_callback( sub {
37 $money_char = $conf->config('money_char') || '$';
42 FS::cust_bill - Object methods for cust_bill records
48 $record = new FS::cust_bill \%hash;
49 $record = new FS::cust_bill { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
59 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
61 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
63 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
65 @cust_pay_objects = $cust_bill->cust_pay;
67 $tax_amount = $record->tax;
69 @lines = $cust_bill->print_text;
70 @lines = $cust_bill->print_text $time;
74 An FS::cust_bill object represents an invoice; a declaration that a customer
75 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
76 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
77 following fields are currently supported:
81 =item invnum - primary key (assigned automatically for new invoices)
83 =item custnum - customer (see L<FS::cust_main>)
85 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
86 L<Time::Local> and L<Date::Parse> for conversion functions.
88 =item charged - amount of this invoice
90 =item printed - deprecated
92 =item closed - books closed flag, empty or `Y'
102 Creates a new invoice. To add the invoice to the database, see L<"insert">.
103 Invoices are normally created by calling the bill method of a customer object
104 (see L<FS::cust_main>).
108 sub table { 'cust_bill'; }
110 sub cust_linked { $_[0]->cust_main_custnum; }
111 sub cust_unlinked_msg {
113 "WARNING: can't find cust_main.custnum ". $self->custnum.
114 ' (cust_bill.invnum '. $self->invnum. ')';
119 Adds this invoice to the database ("Posts" the invoice). If there is an error,
120 returns the error, otherwise returns false.
124 This method now works but you probably shouldn't use it. Instead, apply a
125 credit against the invoice.
127 Using this method to delete invoices outright is really, really bad. There
128 would be no record you ever posted this invoice, and there are no check to
129 make sure charged = 0 or that there are no associated cust_bill_pkg records.
131 Really, don't use it.
137 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
138 $self->SUPER::delete(@_);
141 =item replace OLD_RECORD
143 Replaces the OLD_RECORD with this one in the database. If there is an error,
144 returns the error, otherwise returns false.
146 Only printed may be changed. printed is normally updated by calling the
147 collect method of a customer object (see L<FS::cust_main>).
151 #replace can be inherited from Record.pm
153 # replace_check is now the preferred way to #implement replace data checks
154 # (so $object->replace() works without an argument)
157 my( $new, $old ) = ( shift, shift );
158 return "Can't change custnum!" unless $old->custnum == $new->custnum;
159 #return "Can't change _date!" unless $old->_date eq $new->_date;
160 return "Can't change _date!" unless $old->_date == $new->_date;
161 return "Can't change charged!" unless $old->charged == $new->charged
162 || $old->charged == 0;
169 Checks all fields to make sure this is a valid invoice. If there is an error,
170 returns the error, otherwise returns false. Called by the insert and replace
179 $self->ut_numbern('invnum')
180 || $self->ut_number('custnum')
181 || $self->ut_numbern('_date')
182 || $self->ut_money('charged')
183 || $self->ut_numbern('printed')
184 || $self->ut_enum('closed', [ '', 'Y' ])
186 return $error if $error;
188 return "Unknown customer"
189 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
191 $self->_date(time) unless $self->_date;
193 $self->printed(0) if $self->printed eq '';
200 Returns a list consisting of the total previous balance for this customer,
201 followed by the previous outstanding invoices (as FS::cust_bill objects also).
208 my @cust_bill = sort { $a->_date <=> $b->_date }
209 grep { $_->owed != 0 && $_->_date < $self->_date }
210 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
212 foreach ( @cust_bill ) { $total += $_->owed; }
218 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
224 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
227 =item cust_bill_event
229 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
234 sub cust_bill_event {
236 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
242 Returns the customer (see L<FS::cust_main>) for this invoice.
248 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
251 =item cust_suspend_if_balance_over AMOUNT
253 Suspends the customer associated with this invoice if the total amount owed on
254 this invoice and all older invoices is greater than the specified amount.
256 Returns a list: an empty list on success or a list of errors.
260 sub cust_suspend_if_balance_over {
261 my( $self, $amount ) = ( shift, shift );
262 my $cust_main = $self->cust_main;
263 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
272 Depreciated. See the cust_credited method.
274 #Returns a list consisting of the total previous credited (see
275 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
276 #outstanding credits (FS::cust_credit objects).
282 croak "FS::cust_bill->cust_credit depreciated; see ".
283 "FS::cust_bill->cust_credit_bill";
286 #my @cust_credit = sort { $a->_date <=> $b->_date }
287 # grep { $_->credited != 0 && $_->_date < $self->_date }
288 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
290 #foreach (@cust_credit) { $total += $_->credited; }
291 #$total, @cust_credit;
296 Depreciated. See the cust_bill_pay method.
298 #Returns all payments (see L<FS::cust_pay>) for this invoice.
304 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
306 #sort { $a->_date <=> $b->_date }
307 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
313 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
319 sort { $a->_date <=> $b->_date }
320 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
325 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
331 sort { $a->_date <=> $b->_date }
332 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
338 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
345 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
347 foreach (@taxlines) { $total += $_->setup; }
353 Returns the amount owed (still outstanding) on this invoice, which is charged
354 minus all payment applications (see L<FS::cust_bill_pay>) and credit
355 applications (see L<FS::cust_credit_bill>).
361 my $balance = $self->charged;
362 $balance -= $_->amount foreach ( $self->cust_bill_pay );
363 $balance -= $_->amount foreach ( $self->cust_credited );
364 $balance = sprintf( "%.2f", $balance);
365 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
370 =item generate_email PARAMHASH
372 PARAMHASH can contain the following:
376 =item from => sender address, required
378 =item tempate => alternate template name, optional
380 =item print_text => text attachment arrayref, optional
382 =item subject => email subject, optional
386 Returns an argument list to be passed to L<FS::Misc::send_email>.
397 my $me = '[FS::cust_bill::generate_email]';
400 'from' => $args{'from'},
401 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
404 if (ref($args{'to'} eq 'ARRAY')) {
405 $return{'to'} = $args{'to'};
407 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
408 $self->cust_main->invoicing_list
412 if ( $conf->exists('invoice_html') ) {
414 warn "$me creating HTML/text multipart message"
417 $return{'nobody'} = 1;
419 my $alternative = build MIME::Entity
420 'Type' => 'multipart/alternative',
421 'Encoding' => '7bit',
422 'Disposition' => 'inline'
426 if ( $conf->exists('invoice_email_pdf')
427 and scalar($conf->config('invoice_email_pdf_note')) ) {
429 warn "$me using 'invoice_email_pdf_note' in multipart message"
431 $data = [ map { $_ . "\n" }
432 $conf->config('invoice_email_pdf_note')
437 warn "$me not using 'invoice_email_pdf_note' in multipart message"
439 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
440 $data = $args{'print_text'};
442 $data = [ $self->print_text('', $args{'template'}) ];
447 $alternative->attach(
448 'Type' => 'text/plain',
449 #'Encoding' => 'quoted-printable',
450 'Encoding' => '7bit',
452 'Disposition' => 'inline',
455 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
456 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
458 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
460 if ( defined($args{'_template'}) && length($args{'_template'})
461 && -e "$path/logo_". $args{'_template'}. ".png"
464 $file = "$path/logo_". $args{'_template'}. ".png";
466 $file = "$path/logo.png";
469 my $image = build MIME::Entity
470 'Type' => 'image/png',
471 'Encoding' => 'base64',
473 'Filename' => 'logo.png',
474 'Content-ID' => "<$content_id>",
477 $alternative->attach(
478 'Type' => 'text/html',
479 'Encoding' => 'quoted-printable',
480 'Data' => [ '<html>',
483 ' '. encode_entities($return{'subject'}),
486 ' <body bgcolor="#e8e8e8">',
487 $self->print_html('', $args{'template'}, $content_id),
491 'Disposition' => 'inline',
492 #'Filename' => 'invoice.pdf',
495 if ( $conf->exists('invoice_email_pdf') ) {
500 # multipart/alternative
506 my $related = build MIME::Entity 'Type' => 'multipart/related',
507 'Encoding' => '7bit';
509 #false laziness w/Misc::send_email
510 $related->head->replace('Content-type',
512 '; boundary="'. $related->head->multipart_boundary. '"'.
513 '; type=multipart/alternative'
516 $related->add_part($alternative);
518 $related->add_part($image);
520 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
522 $return{'mimeparts'} = [ $related, $pdf ];
526 #no other attachment:
528 # multipart/alternative
533 $return{'content-type'} = 'multipart/related';
534 $return{'mimeparts'} = [ $alternative, $image ];
535 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
536 #$return{'disposition'} = 'inline';
542 if ( $conf->exists('invoice_email_pdf') ) {
543 warn "$me creating PDF attachment"
546 #mime parts arguments a la MIME::Entity->build().
547 $return{'mimeparts'} = [
548 { $self->mimebuild_pdf('', $args{'template'}) }
552 if ( $conf->exists('invoice_email_pdf')
553 and scalar($conf->config('invoice_email_pdf_note')) ) {
555 warn "$me using 'invoice_email_pdf_note'"
557 $return{'body'} = [ map { $_ . "\n" }
558 $conf->config('invoice_email_pdf_note')
563 warn "$me not using 'invoice_email_pdf_note'"
565 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
566 $return{'body'} = $args{'print_text'};
568 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
581 Returns a list suitable for passing to MIME::Entity->build(), representing
582 this invoice as PDF attachment.
589 'Type' => 'application/pdf',
590 'Encoding' => 'base64',
591 'Data' => [ $self->print_pdf(@_) ],
592 'Disposition' => 'attachment',
593 'Filename' => 'invoice.pdf',
597 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
599 Sends this invoice to the destinations configured for this customer: sends
600 email, prints and/or faxes. See L<FS::cust_main_invoice>.
602 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
604 AGENTNUM, if specified, means that this invoice will only be sent for customers
605 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
606 single agent) or an arrayref of agentnums.
608 INVOICE_FROM, if specified, overrides the default email invoice From: address.
614 my $template = scalar(@_) ? shift : '';
615 if ( scalar(@_) && $_[0] ) {
616 my $agentnums = ref($_[0]) ? shift : [ shift ];
617 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
623 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
625 my @invoicing_list = $self->cust_main->invoicing_list;
627 $self->email($template, $invoice_from)
628 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
630 $self->print($template)
631 if grep { $_ eq 'POST' } @invoicing_list; #postal
633 $self->fax($template)
634 if grep { $_ eq 'FAX' } @invoicing_list; #fax
640 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
644 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
646 INVOICE_FROM, if specified, overrides the default email invoice From: address.
652 my $template = scalar(@_) ? shift : '';
656 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
658 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
659 $self->cust_main->invoicing_list;
661 #better to notify this person than silence
662 @invoicing_list = ($invoice_from) unless @invoicing_list;
664 my $error = send_email(
665 $self->generate_email(
666 'from' => $invoice_from,
667 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
668 'template' => $template,
671 die "can't email invoice: $error\n" if $error;
672 #die "$error\n" if $error;
676 =item lpr_data [ TEMPLATENAME ]
678 Returns the postscript or plaintext for this invoice as an arrayref.
680 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
685 my( $self, $template) = @_;
686 $conf->exists('invoice_latex')
687 ? [ $self->print_ps('', $template) ]
688 : [ $self->print_text('', $template) ];
691 =item print [ TEMPLATENAME ]
695 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
701 my $template = scalar(@_) ? shift : '';
703 my $lpr = $conf->config('lpr');
706 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
708 $outerr = ": $outerr" if length($outerr);
709 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
714 =item fax [ TEMPLATENAME ]
718 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
724 my $template = scalar(@_) ? shift : '';
726 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
727 unless $conf->exists('invoice_latex');
729 my $dialstring = $self->cust_main->getfield('fax');
732 my $error = send_fax( 'docdata' => $self->lpr_data($template),
733 'dialstring' => $dialstring,
735 die $error if $error;
739 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
741 Like B<send>, but only sends the invoice if it is the newest open invoice for
751 grep { $_->owed > 0 }
752 qsearch('cust_bill', {
753 'custnum' => $self->custnum,
754 #'_date' => { op=>'>', value=>$self->_date },
755 'invnum' => { op=>'>', value=>$self->invnum },
762 =item send_csv OPTION => VALUE, ...
764 Sends invoice as a CSV data-file to a remote host with the specified protocol.
768 protocol - currently only "ftp"
774 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
775 and YYMMDDHHMMSS is a timestamp.
777 See L</print_csv> for a description of the output format.
782 my($self, %opt) = @_;
786 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
787 mkdir $spooldir, 0700 unless -d $spooldir;
789 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
790 my $file = "$spooldir/$tracctnum.csv";
792 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
794 open(CSV, ">$file") or die "can't open $file: $!";
802 if ( $opt{protocol} eq 'ftp' ) {
803 eval "use Net::FTP;";
805 $net = Net::FTP->new($opt{server}) or die @$;
807 die "unknown protocol: $opt{protocol}";
810 $net->login( $opt{username}, $opt{password} )
811 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
813 $net->binary or die "can't set binary mode";
815 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
817 $net->put($file) or die "can't put $file: $!";
827 Spools CSV invoice data.
833 =item format - 'default' or 'billco'
835 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
837 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
839 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
846 my($self, %opt) = @_;
848 my $cust_main = $self->cust_main;
850 if ( $opt{'dest'} ) {
851 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
852 $cust_main->invoicing_list;
853 return 'N/A' unless $invoicing_list{$opt{'dest'}}
854 || ! keys %invoicing_list;
857 if ( $opt{'balanceover'} ) {
859 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
862 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
863 mkdir $spooldir, 0700 unless -d $spooldir;
865 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
869 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
870 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
873 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
875 open(CSV, ">>$file") or die "can't open $file: $!";
881 if ( lc($opt{'format'}) eq 'billco' ) {
888 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
891 open(CSV,">>$file") or die "can't open $file: $!";
905 =item print_csv OPTION => VALUE, ...
907 Returns CSV data for this invoice.
911 format - 'default' or 'billco'
913 Returns a list consisting of two scalars. The first is a single line of CSV
914 header information for this invoice. The second is one or more lines of CSV
915 detail information for this invoice.
917 If I<format> is not specified or "default", the fields of the CSV file are as
920 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
924 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
926 B<record_type> is C<cust_bill> for the initial header line only. The
927 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
928 fields are filled in.
930 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
931 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
934 =item invnum - invoice number
936 =item custnum - customer number
938 =item _date - invoice date
940 =item charged - total invoice amount
942 =item first - customer first name
944 =item last - customer first name
946 =item company - company name
948 =item address1 - address line 1
950 =item address2 - address line 1
960 =item pkg - line item description
962 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
964 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
966 =item sdate - start date for recurring fee
968 =item edate - end date for recurring fee
972 If I<format> is "billco", the fields of the header CSV file are as follows:
974 +-------------------------------------------------------------------+
975 | FORMAT HEADER FILE |
976 |-------------------------------------------------------------------|
977 | Field | Description | Name | Type | Width |
978 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
979 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
980 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
981 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
982 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
983 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
984 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
985 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
986 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
987 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
988 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
989 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
990 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
991 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
992 | 15 | Previous Balance | BALFWD | NUM* | 9 |
993 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
994 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
995 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
996 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
997 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
998 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
999 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1000 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1001 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1002 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1003 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1004 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1005 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1006 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1007 +-------+-------------------------------+------------+------+-------+
1009 If I<format> is "billco", the fields of the detail CSV file are as follows:
1011 FORMAT FOR DETAIL FILE
1013 Field | Description | Name | Type | Width
1014 1 | N/A-Leave Empty | RC | CHAR | 2
1015 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1016 3 | Account Number | TRACCTNUM | CHAR | 15
1017 4 | Invoice Number | TRINVOICE | CHAR | 15
1018 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1019 6 | Transaction Detail | DETAILS | CHAR | 100
1020 7 | Amount | AMT | NUM* | 9
1021 8 | Line Format Control** | LNCTRL | CHAR | 2
1022 9 | Grouping Code | GROUP | CHAR | 2
1023 10 | User Defined | ACCT CODE | CHAR | 15
1028 my($self, %opt) = @_;
1030 eval "use Text::CSV_XS";
1033 my $cust_main = $self->cust_main;
1035 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1037 if ( lc($opt{'format'}) eq 'billco' ) {
1040 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1043 if ( $conf->exists('invoice_default_terms')
1044 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1045 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1048 my( $previous_balance, @unused ) = $self->previous; #previous balance
1050 my $pmt_cr_applied = 0;
1051 $pmt_cr_applied += $_->{'amount'}
1052 foreach ( $self->_items_payments, $self->_items_credits ) ;
1054 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1057 '', # 1 | N/A-Leave Empty CHAR 2
1058 '', # 2 | N/A-Leave Empty CHAR 15
1059 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1060 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1061 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1062 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1063 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1064 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1065 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1066 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1067 '', # 10 | Ancillary Billing Information CHAR 30
1068 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1069 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1072 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1075 $duedate, # 14 | Bill Due Date CHAR 10
1077 $previous_balance, # 15 | Previous Balance NUM* 9
1078 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1079 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1080 $totaldue, # 18 | Total Amt Due NUM* 9
1081 $totaldue, # 19 | Total Amt Due NUM* 9
1082 '', # 20 | 30 Day Aging NUM* 9
1083 '', # 21 | 60 Day Aging NUM* 9
1084 '', # 22 | 90 Day Aging NUM* 9
1085 'N', # 23 | Y/N CHAR 1
1086 '', # 24 | Remittance automation CHAR 100
1087 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1088 $self->custnum, # 26 | Customer Reference Number CHAR 15
1089 '0', # 27 | Federal Tax*** NUM* 9
1090 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1091 '0', # 29 | Other Taxes & Fees*** NUM* 9
1100 time2str("%x", $self->_date),
1101 sprintf("%.2f", $self->charged),
1102 ( map { $cust_main->getfield($_) }
1103 qw( first last company address1 address2 city state zip country ) ),
1105 ) or die "can't create csv";
1108 my $header = $csv->string. "\n";
1111 if ( lc($opt{'format'}) eq 'billco' ) {
1114 foreach my $item ( $self->_items_pkg ) {
1117 '', # 1 | N/A-Leave Empty CHAR 2
1118 '', # 2 | N/A-Leave Empty CHAR 15
1119 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1120 $self->invnum, # 4 | Invoice Number CHAR 15
1121 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1122 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1123 $item->{'amount'}, # 7 | Amount NUM* 9
1124 '', # 8 | Line Format Control** CHAR 2
1125 '', # 9 | Grouping Code CHAR 2
1126 '', # 10 | User Defined CHAR 15
1129 $detail .= $csv->string. "\n";
1135 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1137 my($pkg, $setup, $recur, $sdate, $edate);
1138 if ( $cust_bill_pkg->pkgnum ) {
1140 ($pkg, $setup, $recur, $sdate, $edate) = (
1141 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1142 ( $cust_bill_pkg->setup != 0
1143 ? sprintf("%.2f", $cust_bill_pkg->setup )
1145 ( $cust_bill_pkg->recur != 0
1146 ? sprintf("%.2f", $cust_bill_pkg->recur )
1148 ( $cust_bill_pkg->sdate
1149 ? time2str("%x", $cust_bill_pkg->sdate)
1151 ($cust_bill_pkg->edate
1152 ?time2str("%x", $cust_bill_pkg->edate)
1156 } else { #pkgnum tax
1157 next unless $cust_bill_pkg->setup != 0;
1158 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1159 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1161 ($pkg, $setup, $recur, $sdate, $edate) =
1162 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1168 ( map { '' } (1..11) ),
1169 ($pkg, $setup, $recur, $sdate, $edate)
1170 ) or die "can't create csv";
1172 $detail .= $csv->string. "\n";
1178 ( $header, $detail );
1184 Pays this invoice with a compliemntary payment. If there is an error,
1185 returns the error, otherwise returns false.
1191 my $cust_pay = new FS::cust_pay ( {
1192 'invnum' => $self->invnum,
1193 'paid' => $self->owed,
1196 'payinfo' => $self->cust_main->payinfo,
1204 Attempts to pay this invoice with a credit card payment via a
1205 Business::OnlinePayment realtime gateway. See
1206 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1207 for supported processors.
1213 $self->realtime_bop( 'CC', @_ );
1218 Attempts to pay this invoice with an electronic check (ACH) payment via a
1219 Business::OnlinePayment realtime gateway. See
1220 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1221 for supported processors.
1227 $self->realtime_bop( 'ECHECK', @_ );
1232 Attempts to pay this invoice with phone bill (LEC) payment via a
1233 Business::OnlinePayment realtime gateway. See
1234 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1235 for supported processors.
1241 $self->realtime_bop( 'LEC', @_ );
1245 my( $self, $method ) = @_;
1247 my $cust_main = $self->cust_main;
1248 my $balance = $cust_main->balance;
1249 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1250 $amount = sprintf("%.2f", $amount);
1251 return "not run (balance $balance)" unless $amount > 0;
1253 my $description = 'Internet Services';
1254 if ( $conf->exists('business-onlinepayment-description') ) {
1255 my $dtempl = $conf->config('business-onlinepayment-description');
1257 my $agent_obj = $cust_main->agent
1258 or die "can't retreive agent for $cust_main (agentnum ".
1259 $cust_main->agentnum. ")";
1260 my $agent = $agent_obj->agent;
1261 my $pkgs = join(', ',
1262 map { $_->cust_pkg->part_pkg->pkg }
1263 grep { $_->pkgnum } $self->cust_bill_pkg
1265 $description = eval qq("$dtempl");
1268 $cust_main->realtime_bop($method, $amount,
1269 'description' => $description,
1270 'invnum' => $self->invnum,
1277 Adds a payment for this invoice to the pending credit card batch (see
1278 L<FS::cust_pay_batch>).
1284 my $cust_main = $self->cust_main;
1285 my $oldAutoCommit = $FS::UID::AutoCommit;
1286 local $FS::UID::AutoCommit = 0;
1289 my $pay_batch = qsearchs('pay_batch'=> '');
1291 unless ($pay_batch) {
1292 $pay_batch = new FS::pay_batch;
1293 my $error = $pay_batch->insert;
1295 die "error creating new batch: $error\n";
1299 my $cust_pay_batch = new FS::cust_pay_batch ( {
1300 'batchnum' => $pay_batch->getfield('batchnum'),
1301 'invnum' => $self->getfield('invnum'),
1302 'custnum' => $cust_main->getfield('custnum'),
1303 'last' => $cust_main->getfield('last'),
1304 'first' => $cust_main->getfield('first'),
1305 'address1' => $cust_main->getfield('address1'),
1306 'address2' => $cust_main->getfield('address2'),
1307 'city' => $cust_main->getfield('city'),
1308 'state' => $cust_main->getfield('state'),
1309 'zip' => $cust_main->getfield('zip'),
1310 'country' => $cust_main->getfield('country'),
1311 'payinfo' => $cust_main->payinfo,
1312 'exp' => $cust_main->getfield('paydate'),
1313 'payname' => $cust_main->getfield('payname'),
1314 'amount' => $self->owed,
1316 my $error = $cust_pay_batch->insert;
1317 die $error if $error;
1319 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1324 sub _agent_template {
1326 $self->_agent_plandata('agent_templatename');
1329 sub _agent_invoice_from {
1331 $self->_agent_plandata('agent_invoice_from');
1334 sub _agent_plandata {
1335 my( $self, $option ) = @_;
1337 my $part_bill_event = qsearchs( 'part_bill_event',
1339 'payby' => $self->cust_main->payby,
1340 'plan' => 'send_agent',
1341 'plandata' => { 'op' => '~',
1342 'value' => "(^|\n)agentnum ".
1344 $self->cust_main->agentnum.
1350 'ORDER BY seconds LIMIT 1'
1353 return '' unless $part_bill_event;
1355 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1358 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1359 " plandata for $option";
1365 =item print_text [ TIME [ , TEMPLATE ] ]
1367 Returns an text invoice, as a list of lines.
1369 TIME an optional value used to control the printing of overdue messages. The
1370 default is now. It isn't the date of the invoice; that's the `_date' field.
1371 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1372 L<Time::Local> and L<Date::Parse> for conversion functions.
1376 #still some false laziness w/_items stuff (and send_csv)
1379 my( $self, $today, $template ) = @_;
1382 # my $invnum = $self->invnum;
1383 my $cust_main = $self->cust_main;
1384 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1385 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1387 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1388 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1389 #my $balance_due = $self->owed + $pr_total - $cr_total;
1390 my $balance_due = $self->owed + $pr_total;
1393 #my($description,$amount);
1397 foreach ( @pr_cust_bill ) {
1399 "Previous Balance, Invoice #". $_->invnum.
1400 " (". time2str("%x",$_->_date). ")",
1401 $money_char. sprintf("%10.2f",$_->owed)
1404 if (@pr_cust_bill) {
1405 push @buf,['','-----------'];
1406 push @buf,[ 'Total Previous Balance',
1407 $money_char. sprintf("%10.2f",$pr_total ) ];
1412 foreach my $cust_bill_pkg (
1413 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1414 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1417 my $desc = $cust_bill_pkg->desc;
1419 if ( $cust_bill_pkg->pkgnum > 0 ) {
1421 if ( $cust_bill_pkg->setup != 0 ) {
1422 my $description = $desc;
1423 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1424 push @buf, [ $description,
1425 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1427 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1428 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1431 if ( $cust_bill_pkg->recur != 0 ) {
1433 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1434 time2str("%x", $cust_bill_pkg->edate) . ")",
1435 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1438 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1439 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1440 $cust_bill_pkg->sdate );
1443 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1445 } else { #pkgnum tax or one-shot line item
1447 if ( $cust_bill_pkg->setup != 0 ) {
1449 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1451 if ( $cust_bill_pkg->recur != 0 ) {
1452 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1453 . time2str("%x", $cust_bill_pkg->edate). ")",
1454 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1462 push @buf,['','-----------'];
1463 push @buf,['Total New Charges',
1464 $money_char. sprintf("%10.2f",$self->charged) ];
1467 push @buf,['','-----------'];
1468 push @buf,['Total Charges',
1469 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1473 foreach ( $self->cust_credited ) {
1475 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1477 my $reason = substr($_->cust_credit->reason,0,32);
1478 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1479 $reason = " ($reason) " if $reason;
1481 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1483 $money_char. sprintf("%10.2f",$_->amount)
1486 #foreach ( @cr_cust_credit ) {
1488 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1489 # $money_char. sprintf("%10.2f",$_->credited)
1493 #get & print payments
1494 foreach ( $self->cust_bill_pay ) {
1496 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1499 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1500 $money_char. sprintf("%10.2f",$_->amount )
1505 my $balance_due_msg = $self->balance_due_msg;
1507 push @buf,['','-----------'];
1508 push @buf,[$balance_due_msg, $money_char.
1509 sprintf("%10.2f", $balance_due ) ];
1511 #create the template
1512 $template ||= $self->_agent_template;
1513 my $templatefile = 'invoice_template';
1514 $templatefile .= "_$template" if length($template);
1515 my @invoice_template = $conf->config($templatefile)
1516 or die "cannot load config file $templatefile";
1519 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1520 /invoice_lines\((\d*)\)/;
1521 $invoice_lines += $1 || scalar(@buf);
1524 die "no invoice_lines() functions in template?" unless $wasfunc;
1525 my $invoice_template = new Text::Template (
1527 SOURCE => [ map "$_\n", @invoice_template ],
1528 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1529 $invoice_template->compile()
1530 or die "can't compile template: $Text::Template::ERROR";
1532 #setup template variables
1533 package FS::cust_bill::_template; #!
1534 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1536 $invnum = $self->invnum;
1537 $date = $self->_date;
1539 $agent = $self->cust_main->agent->agent;
1541 if ( $FS::cust_bill::invoice_lines ) {
1543 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1545 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1550 #format address (variable for the template)
1552 @address = ( '', '', '', '', '', '' );
1553 package FS::cust_bill; #!
1554 $FS::cust_bill::_template::address[$l++] =
1555 $cust_main->payname.
1556 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1557 ? " (P.O. #". $cust_main->payinfo. ")"
1561 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1562 if $cust_main->company;
1563 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1564 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1565 if $cust_main->address2;
1566 $FS::cust_bill::_template::address[$l++] =
1567 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1569 my $countrydefault = $conf->config('countrydefault') || 'US';
1570 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1571 unless $cust_main->country eq $countrydefault;
1573 # #overdue? (variable for the template)
1574 # $FS::cust_bill::_template::overdue = (
1576 # && $today > $self->_date
1577 ## && $self->printed > 1
1578 # && $self->printed > 0
1581 #and subroutine for the template
1582 sub FS::cust_bill::_template::invoice_lines {
1583 my $lines = shift || scalar(@buf);
1585 scalar(@buf) ? shift @buf : [ '', '' ];
1591 $FS::cust_bill::_template::page = 1;
1595 push @collect, split("\n",
1596 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1598 $FS::cust_bill::_template::page++;
1601 map "$_\n", @collect;
1605 =item print_latex [ TIME [ , TEMPLATE ] ]
1607 Internal method - returns a filename of a filled-in LaTeX template for this
1608 invoice (Note: add ".tex" to get the actual filename).
1610 See print_ps and print_pdf for methods that return PostScript and PDF output.
1612 TIME an optional value used to control the printing of overdue messages. The
1613 default is now. It isn't the date of the invoice; that's the `_date' field.
1614 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1615 L<Time::Local> and L<Date::Parse> for conversion functions.
1619 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1622 my( $self, $today, $template ) = @_;
1624 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1627 my $cust_main = $self->cust_main;
1628 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1629 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1631 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1632 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1633 #my $balance_due = $self->owed + $pr_total - $cr_total;
1634 my $balance_due = $self->owed + $pr_total;
1636 #create the template
1637 $template ||= $self->_agent_template;
1638 my $templatefile = 'invoice_latex';
1639 my $suffix = length($template) ? "_$template" : '';
1640 $templatefile .= $suffix;
1641 my @invoice_template = map "$_\n", $conf->config($templatefile)
1642 or die "cannot load config file $templatefile";
1644 my($format, $text_template);
1645 if ( grep { /^%%Detail/ } @invoice_template ) {
1646 #change this to a die when the old code is removed
1647 warn "old-style invoice template $templatefile; ".
1648 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1651 $format = 'Text::Template';
1652 $text_template = new Text::Template(
1654 SOURCE => \@invoice_template,
1655 DELIMITERS => [ '[@--', '--@]' ],
1658 $text_template->compile()
1659 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1663 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1664 $returnaddress = join("\n",
1665 $conf->config_orbase('invoice_latexreturnaddress', $template)
1668 $returnaddress = '~';
1671 my %invoice_data = (
1672 'invnum' => $self->invnum,
1673 'date' => time2str('%b %o, %Y', $self->_date),
1674 'today' => time2str('%b %o, %Y', $today),
1675 'agent' => _latex_escape($cust_main->agent->agent),
1676 'payname' => _latex_escape($cust_main->payname),
1677 'company' => _latex_escape($cust_main->company),
1678 'address1' => _latex_escape($cust_main->address1),
1679 'address2' => _latex_escape($cust_main->address2),
1680 'city' => _latex_escape($cust_main->city),
1681 'state' => _latex_escape($cust_main->state),
1682 'zip' => _latex_escape($cust_main->zip),
1683 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1684 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1685 'returnaddress' => $returnaddress,
1687 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1688 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1689 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1692 my $countrydefault = $conf->config('countrydefault') || 'US';
1693 if ( $cust_main->country eq $countrydefault ) {
1694 $invoice_data{'country'} = '';
1696 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1699 $invoice_data{'notes'} =
1701 # #do variable substitutions in notes
1702 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1703 $conf->config_orbase('invoice_latexnotes', $template)
1705 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1708 $invoice_data{'footer'} =~ s/\n+$//;
1709 $invoice_data{'smallfooter'} =~ s/\n+$//;
1710 $invoice_data{'notes'} =~ s/\n+$//;
1712 $invoice_data{'po_line'} =
1713 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1714 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1718 if ( $format eq 'old' ) {
1721 my @total_item = ();
1722 while ( @invoice_template ) {
1723 my $line = shift @invoice_template;
1725 if ( $line =~ /^%%Detail\s*$/ ) {
1727 while ( ( my $line_item_line = shift @invoice_template )
1728 !~ /^%%EndDetail\s*$/ ) {
1729 push @line_item, $line_item_line;
1731 foreach my $line_item ( $self->_items ) {
1732 #foreach my $line_item ( $self->_items_pkg ) {
1733 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1734 $invoice_data{'description'} =
1735 _latex_escape($line_item->{'description'});
1736 if ( exists $line_item->{'ext_description'} ) {
1737 $invoice_data{'description'} .=
1738 "\\tabularnewline\n~~".
1739 join( "\\tabularnewline\n~~",
1740 map _latex_escape($_), @{$line_item->{'ext_description'}}
1743 $invoice_data{'amount'} = $line_item->{'amount'};
1744 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1746 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1749 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1751 while ( ( my $total_item_line = shift @invoice_template )
1752 !~ /^%%EndTotalDetails\s*$/ ) {
1753 push @total_item, $total_item_line;
1756 my @total_fill = ();
1759 foreach my $tax ( $self->_items_tax ) {
1760 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1761 $taxtotal += $tax->{'amount'};
1762 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1764 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1769 $invoice_data{'total_item'} = 'Sub-total';
1770 $invoice_data{'total_amount'} =
1771 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1772 unshift @total_fill,
1773 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1777 $invoice_data{'total_item'} = '\textbf{Total}';
1778 $invoice_data{'total_amount'} =
1779 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1781 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1784 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1787 foreach my $credit ( $self->_items_credits ) {
1788 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1790 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1792 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1797 foreach my $payment ( $self->_items_payments ) {
1798 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1800 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1802 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1806 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1807 $invoice_data{'total_amount'} =
1808 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1810 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1813 push @filled_in, @total_fill;
1816 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1817 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1818 push @filled_in, $line;
1829 } elsif ( $format eq 'Text::Template' ) {
1831 my @detail_items = ();
1832 my @total_items = ();
1834 $invoice_data{'detail_items'} = \@detail_items;
1835 $invoice_data{'total_items'} = \@total_items;
1837 foreach my $line_item ( $self->_items ) {
1839 ext_description => [],
1841 $detail->{'ref'} = $line_item->{'pkgnum'};
1842 $detail->{'quantity'} = 1;
1843 $detail->{'description'} = _latex_escape($line_item->{'description'});
1844 if ( exists $line_item->{'ext_description'} ) {
1845 @{$detail->{'ext_description'}} = map {
1847 } @{$line_item->{'ext_description'}};
1849 $detail->{'amount'} = $line_item->{'amount'};
1850 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1852 push @detail_items, $detail;
1857 foreach my $tax ( $self->_items_tax ) {
1859 $total->{'total_item'} = _latex_escape($tax->{'description'});
1860 $taxtotal += $tax->{'amount'};
1861 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1862 push @total_items, $total;
1867 $total->{'total_item'} = 'Sub-total';
1868 $total->{'total_amount'} =
1869 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1870 unshift @total_items, $total;
1875 $total->{'total_item'} = '\textbf{Total}';
1876 $total->{'total_amount'} =
1877 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1878 push @total_items, $total;
1881 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1884 foreach my $credit ( $self->_items_credits ) {
1886 $total->{'total_item'} = _latex_escape($credit->{'description'});
1888 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1889 push @total_items, $total;
1893 foreach my $payment ( $self->_items_payments ) {
1895 $total->{'total_item'} = _latex_escape($payment->{'description'});
1897 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1898 push @total_items, $total;
1903 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1904 $total->{'total_amount'} =
1905 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1906 push @total_items, $total;
1910 die "guru meditation #54";
1913 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1914 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1918 ) or die "can't open temp file: $!\n";
1919 if ( $format eq 'old' ) {
1920 print $fh join('', @filled_in );
1921 } elsif ( $format eq 'Text::Template' ) {
1922 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1924 die "guru meditation #32";
1928 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1933 =item print_ps [ TIME [ , TEMPLATE ] ]
1935 Returns an postscript invoice, as a scalar.
1937 TIME an optional value used to control the printing of overdue messages. The
1938 default is now. It isn't the date of the invoice; that's the `_date' field.
1939 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1940 L<Time::Local> and L<Date::Parse> for conversion functions.
1947 my $file = $self->print_latex(@_);
1949 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1952 my $sfile = shell_quote $file;
1954 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1955 or die "pslatex $file.tex failed; see $file.log for details?\n";
1956 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1957 or die "pslatex $file.tex failed; see $file.log for details?\n";
1959 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1960 or die "dvips failed";
1962 open(POSTSCRIPT, "<$file.ps")
1963 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1965 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1968 while (<POSTSCRIPT>) {
1978 =item print_pdf [ TIME [ , TEMPLATE ] ]
1980 Returns an PDF invoice, as a scalar.
1982 TIME an optional value used to control the printing of overdue messages. The
1983 default is now. It isn't the date of the invoice; that's the `_date' field.
1984 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1985 L<Time::Local> and L<Date::Parse> for conversion functions.
1992 my $file = $self->print_latex(@_);
1994 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1997 #system('pdflatex', "$file.tex");
1998 #system('pdflatex', "$file.tex");
1999 #! LaTeX Error: Unknown graphics extension: .eps.
2001 my $sfile = shell_quote $file;
2003 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2004 or die "pslatex $file.tex failed; see $file.log for details?\n";
2005 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2006 or die "pslatex $file.tex failed; see $file.log for details?\n";
2008 #system('dvipdf', "$file.dvi", "$file.pdf" );
2010 "dvips -q -t letter -f $sfile.dvi ".
2011 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2014 or die "dvips | gs failed: $!";
2016 open(PDF, "<$file.pdf")
2017 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2019 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2032 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2034 Returns an HTML invoice, as a scalar.
2036 TIME an optional value used to control the printing of overdue messages. The
2037 default is now. It isn't the date of the invoice; that's the `_date' field.
2038 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2039 L<Time::Local> and L<Date::Parse> for conversion functions.
2041 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2042 when emailing the invoice as part of a multipart/related MIME email.
2046 #some falze laziness w/print_text and print_latex (and send_csv)
2048 my( $self, $today, $template, $cid ) = @_;
2051 my $cust_main = $self->cust_main;
2052 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2053 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2055 $template ||= $self->_agent_template;
2056 my $templatefile = 'invoice_html';
2057 my $suffix = length($template) ? "_$template" : '';
2058 $templatefile .= $suffix;
2059 my @html_template = map "$_\n", $conf->config($templatefile)
2060 or die "cannot load config file $templatefile";
2062 my $html_template = new Text::Template(
2064 SOURCE => \@html_template,
2065 DELIMITERS => [ '<%=', '%>' ],
2068 $html_template->compile()
2069 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2071 my %invoice_data = (
2072 'invnum' => $self->invnum,
2073 'date' => time2str('%b %o, %Y', $self->_date),
2074 'today' => time2str('%b %o, %Y', $today),
2075 'agent' => encode_entities($cust_main->agent->agent),
2076 'payname' => encode_entities($cust_main->payname),
2077 'company' => encode_entities($cust_main->company),
2078 'address1' => encode_entities($cust_main->address1),
2079 'address2' => encode_entities($cust_main->address2),
2080 'city' => encode_entities($cust_main->city),
2081 'state' => encode_entities($cust_main->state),
2082 'zip' => encode_entities($cust_main->zip),
2083 'terms' => $conf->config('invoice_default_terms')
2084 || 'Payable upon receipt',
2086 'template' => $template,
2087 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2091 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2092 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2094 $invoice_data{'returnaddress'} =
2095 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2097 $invoice_data{'returnaddress'} =
2100 s/\\\\\*?\s*$/<BR>/;
2101 s/\\hyphenation\{[\w\s\-]+\}//;
2104 $conf->config_orbase( 'invoice_latexreturnaddress',
2110 my $countrydefault = $conf->config('countrydefault') || 'US';
2111 if ( $cust_main->country eq $countrydefault ) {
2112 $invoice_data{'country'} = '';
2114 $invoice_data{'country'} =
2115 encode_entities(code2country($cust_main->country));
2119 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2120 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2122 $invoice_data{'notes'} =
2123 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2125 $invoice_data{'notes'} =
2127 s/%%(.*)$/<!-- $1 -->/;
2128 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2129 s/\\begin\{enumerate\}/<ol>/;
2131 s/\\end\{enumerate\}/<\/ol>/;
2132 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2135 $conf->config_orbase('invoice_latexnotes', $template)
2139 # #do variable substitutions in notes
2140 # $invoice_data{'notes'} =
2142 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2143 # $conf->config_orbase('invoice_latexnotes', $suffix)
2147 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2148 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2150 $invoice_data{'footer'} =
2151 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2153 $invoice_data{'footer'} =
2154 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2155 $conf->config_orbase('invoice_latexfooter', $template)
2159 $invoice_data{'po_line'} =
2160 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2161 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2164 my $money_char = $conf->config('money_char') || '$';
2166 foreach my $line_item ( $self->_items ) {
2168 ext_description => [],
2170 $detail->{'ref'} = $line_item->{'pkgnum'};
2171 $detail->{'description'} = encode_entities($line_item->{'description'});
2172 if ( exists $line_item->{'ext_description'} ) {
2173 @{$detail->{'ext_description'}} = map {
2174 encode_entities($_);
2175 } @{$line_item->{'ext_description'}};
2177 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2178 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2180 push @{$invoice_data{'detail_items'}}, $detail;
2185 foreach my $tax ( $self->_items_tax ) {
2187 $total->{'total_item'} = encode_entities($tax->{'description'});
2188 $taxtotal += $tax->{'amount'};
2189 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2190 push @{$invoice_data{'total_items'}}, $total;
2195 $total->{'total_item'} = 'Sub-total';
2196 $total->{'total_amount'} =
2197 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2198 unshift @{$invoice_data{'total_items'}}, $total;
2201 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2204 $total->{'total_item'} = '<b>Total</b>';
2205 $total->{'total_amount'} =
2206 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2207 push @{$invoice_data{'total_items'}}, $total;
2210 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2213 foreach my $credit ( $self->_items_credits ) {
2215 $total->{'total_item'} = encode_entities($credit->{'description'});
2217 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2218 push @{$invoice_data{'total_items'}}, $total;
2222 foreach my $payment ( $self->_items_payments ) {
2224 $total->{'total_item'} = encode_entities($payment->{'description'});
2226 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2227 push @{$invoice_data{'total_items'}}, $total;
2232 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2233 $total->{'total_amount'} =
2234 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2235 push @{$invoice_data{'total_items'}}, $total;
2238 $html_template->fill_in( HASH => \%invoice_data);
2241 # quick subroutine for print_latex
2243 # There are ten characters that LaTeX treats as special characters, which
2244 # means that they do not simply typeset themselves:
2245 # # $ % & ~ _ ^ \ { }
2247 # TeX ignores blanks following an escaped character; if you want a blank (as
2248 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2252 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2253 $value =~ s/([<>])/\$$1\$/g;
2257 #utility methods for print_*
2259 sub balance_due_msg {
2261 my $msg = 'Balance Due';
2262 return $msg unless $conf->exists('invoice_default_terms');
2263 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2264 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2265 } elsif ( $conf->config('invoice_default_terms') ) {
2266 $msg .= ' - '. $conf->config('invoice_default_terms');
2273 my @display = scalar(@_)
2275 : qw( _items_previous _items_pkg );
2276 #: qw( _items_pkg );
2277 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2279 foreach my $display ( @display ) {
2280 push @b, $self->$display(@_);
2285 sub _items_previous {
2287 my $cust_main = $self->cust_main;
2288 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2290 foreach ( @pr_cust_bill ) {
2292 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2293 ' ('. time2str('%x',$_->_date). ')',
2294 #'pkgpart' => 'N/A',
2296 'amount' => sprintf("%.2f", $_->owed),
2302 # 'description' => 'Previous Balance',
2303 # #'pkgpart' => 'N/A',
2304 # 'pkgnum' => 'N/A',
2305 # 'amount' => sprintf("%10.2f", $pr_total ),
2306 # 'ext_description' => [ map {
2307 # "Invoice ". $_->invnum.
2308 # " (". time2str("%x",$_->_date). ") ".
2309 # sprintf("%10.2f", $_->owed)
2310 # } @pr_cust_bill ],
2317 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2318 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2323 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2324 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2327 sub _items_cust_bill_pkg {
2329 my $cust_bill_pkg = shift;
2332 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2334 my $desc = $cust_bill_pkg->desc;
2336 if ( $cust_bill_pkg->pkgnum > 0 ) {
2338 if ( $cust_bill_pkg->setup != 0 ) {
2339 my $description = $desc;
2340 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2341 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2342 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2344 description => $description,
2345 #pkgpart => $part_pkg->pkgpart,
2346 pkgnum => $cust_bill_pkg->pkgnum,
2347 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2348 ext_description => \@d,
2352 if ( $cust_bill_pkg->recur != 0 ) {
2354 description => "$desc (" .
2355 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2356 time2str('%x', $cust_bill_pkg->edate). ')',
2357 #pkgpart => $part_pkg->pkgpart,
2358 pkgnum => $cust_bill_pkg->pkgnum,
2359 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2361 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2362 $cust_bill_pkg->sdate),
2363 $cust_bill_pkg->details,
2368 } else { #pkgnum tax or one-shot line item (??)
2370 if ( $cust_bill_pkg->setup != 0 ) {
2372 'description' => $desc,
2373 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2376 if ( $cust_bill_pkg->recur != 0 ) {
2378 'description' => "$desc (".
2379 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2380 time2str("%x", $cust_bill_pkg->edate). ')',
2381 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2393 sub _items_credits {
2398 foreach ( $self->cust_credited ) {
2400 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2402 my $reason = $_->cust_credit->reason;
2403 #my $reason = substr($_->cust_credit->reason,0,32);
2404 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2405 $reason = " ($reason) " if $reason;
2407 #'description' => 'Credit ref\#'. $_->crednum.
2408 # " (". time2str("%x",$_->cust_credit->_date) .")".
2410 'description' => 'Credit applied '.
2411 time2str("%x",$_->cust_credit->_date). $reason,
2412 'amount' => sprintf("%.2f",$_->amount),
2415 #foreach ( @cr_cust_credit ) {
2417 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2418 # $money_char. sprintf("%10.2f",$_->credited)
2426 sub _items_payments {
2430 #get & print payments
2431 foreach ( $self->cust_bill_pay ) {
2433 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2436 'description' => "Payment received ".
2437 time2str("%x",$_->cust_pay->_date ),
2438 'amount' => sprintf("%.2f", $_->amount )
2457 sub process_reprint {
2458 process_re_X('print', @_);
2465 sub process_reemail {
2466 process_re_X('email', @_);
2474 process_re_X('fax', @_);
2477 use Storable qw(thaw);
2481 my( $method, $job ) = ( shift, shift );
2482 warn "process_re_X $method for job $job\n" if $DEBUG;
2484 my $param = thaw(decode_base64(shift));
2485 warn Dumper($param) if $DEBUG;
2496 my($method, $job, %param ) = @_;
2497 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2499 warn "re_X $method for job $job with param:\n".
2500 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2503 #some false laziness w/search/cust_bill.html
2505 my $orderby = 'ORDER BY cust_bill._date';
2509 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2510 push @where, "cust_bill._date >= $1";
2512 if ( $param{'end'} =~ /^(\d+)$/ ) {
2513 push @where, "cust_bill._date < $1";
2515 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2516 push @where, "cust_main.agentnum = $1";
2520 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2521 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2522 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2523 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2525 push @where, "0 != $owed"
2528 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2531 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2533 my $addl_from = 'left join cust_main using ( custnum )';
2535 if ( $param{'newest_percust'} ) {
2536 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2537 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2538 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2541 my @cust_bill = qsearch( 'cust_bill',
2543 "$distinct cust_bill.*",
2549 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2550 foreach my $cust_bill ( @cust_bill ) {
2551 $cust_bill->$method();
2553 if ( $job ) { #progressbar foo
2555 if ( time - $min_sec > $last ) {
2556 my $error = $job->update_statustext(
2557 int( 100 * $num / scalar(@cust_bill) )
2559 die $error if $error;
2574 print_text formatting (and some logic :/) is in source, but needs to be
2575 slurped in from a file. Also number of lines ($=).
2579 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2580 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base