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;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
28 use FS::cust_bill_pay;
29 use FS::part_bill_event;
31 @ISA = qw( FS::cust_main_Mixin FS::Record );
35 #ask FS::UID to run this stuff for us later
36 FS::UID->install_callback( sub {
38 $money_char = $conf->config('money_char') || '$';
43 FS::cust_bill - Object methods for cust_bill records
49 $record = new FS::cust_bill \%hash;
50 $record = new FS::cust_bill { 'column' => 'value' };
52 $error = $record->insert;
54 $error = $new_record->replace($old_record);
56 $error = $record->delete;
58 $error = $record->check;
60 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
62 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
64 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
66 @cust_pay_objects = $cust_bill->cust_pay;
68 $tax_amount = $record->tax;
70 @lines = $cust_bill->print_text;
71 @lines = $cust_bill->print_text $time;
75 An FS::cust_bill object represents an invoice; a declaration that a customer
76 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
77 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
78 following fields are currently supported:
82 =item invnum - primary key (assigned automatically for new invoices)
84 =item custnum - customer (see L<FS::cust_main>)
86 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
87 L<Time::Local> and L<Date::Parse> for conversion functions.
89 =item charged - amount of this invoice
91 =item printed - deprecated
93 =item closed - books closed flag, empty or `Y'
103 Creates a new invoice. To add the invoice to the database, see L<"insert">.
104 Invoices are normally created by calling the bill method of a customer object
105 (see L<FS::cust_main>).
109 sub table { 'cust_bill'; }
111 sub cust_linked { $_[0]->cust_main_custnum; }
112 sub cust_unlinked_msg {
114 "WARNING: can't find cust_main.custnum ". $self->custnum.
115 ' (cust_bill.invnum '. $self->invnum. ')';
120 Adds this invoice to the database ("Posts" the invoice). If there is an error,
121 returns the error, otherwise returns false.
125 This method now works but you probably shouldn't use it. Instead, apply a
126 credit against the invoice.
128 Using this method to delete invoices outright is really, really bad. There
129 would be no record you ever posted this invoice, and there are no check to
130 make sure charged = 0 or that there are no associated cust_bill_pkg records.
132 Really, don't use it.
138 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
139 $self->SUPER::delete(@_);
142 =item replace OLD_RECORD
144 Replaces the OLD_RECORD with this one in the database. If there is an error,
145 returns the error, otherwise returns false.
147 Only printed may be changed. printed is normally updated by calling the
148 collect method of a customer object (see L<FS::cust_main>).
152 #replace can be inherited from Record.pm
154 # replace_check is now the preferred way to #implement replace data checks
155 # (so $object->replace() works without an argument)
158 my( $new, $old ) = ( shift, shift );
159 return "Can't change custnum!" unless $old->custnum == $new->custnum;
160 #return "Can't change _date!" unless $old->_date eq $new->_date;
161 return "Can't change _date!" unless $old->_date == $new->_date;
162 return "Can't change charged!" unless $old->charged == $new->charged
163 || $old->charged == 0;
170 Checks all fields to make sure this is a valid invoice. If there is an error,
171 returns the error, otherwise returns false. Called by the insert and replace
180 $self->ut_numbern('invnum')
181 || $self->ut_number('custnum')
182 || $self->ut_numbern('_date')
183 || $self->ut_money('charged')
184 || $self->ut_numbern('printed')
185 || $self->ut_enum('closed', [ '', 'Y' ])
187 return $error if $error;
189 return "Unknown customer"
190 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
192 $self->_date(time) unless $self->_date;
194 $self->printed(0) if $self->printed eq '';
201 Returns a list consisting of the total previous balance for this customer,
202 followed by the previous outstanding invoices (as FS::cust_bill objects also).
209 my @cust_bill = sort { $a->_date <=> $b->_date }
210 grep { $_->owed != 0 && $_->_date < $self->_date }
211 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
213 foreach ( @cust_bill ) { $total += $_->owed; }
219 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
225 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
228 =item cust_bill_event
230 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
235 sub cust_bill_event {
237 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
243 Returns the customer (see L<FS::cust_main>) for this invoice.
249 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
252 =item cust_suspend_if_balance_over AMOUNT
254 Suspends the customer associated with this invoice if the total amount owed on
255 this invoice and all older invoices is greater than the specified amount.
257 Returns a list: an empty list on success or a list of errors.
261 sub cust_suspend_if_balance_over {
262 my( $self, $amount ) = ( shift, shift );
263 my $cust_main = $self->cust_main;
264 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
273 Depreciated. See the cust_credited method.
275 #Returns a list consisting of the total previous credited (see
276 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
277 #outstanding credits (FS::cust_credit objects).
283 croak "FS::cust_bill->cust_credit depreciated; see ".
284 "FS::cust_bill->cust_credit_bill";
287 #my @cust_credit = sort { $a->_date <=> $b->_date }
288 # grep { $_->credited != 0 && $_->_date < $self->_date }
289 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
291 #foreach (@cust_credit) { $total += $_->credited; }
292 #$total, @cust_credit;
297 Depreciated. See the cust_bill_pay method.
299 #Returns all payments (see L<FS::cust_pay>) for this invoice.
305 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
307 #sort { $a->_date <=> $b->_date }
308 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
314 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
320 sort { $a->_date <=> $b->_date }
321 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
326 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
332 sort { $a->_date <=> $b->_date }
333 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
339 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
346 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
348 foreach (@taxlines) { $total += $_->setup; }
354 Returns the amount owed (still outstanding) on this invoice, which is charged
355 minus all payment applications (see L<FS::cust_bill_pay>) and credit
356 applications (see L<FS::cust_credit_bill>).
362 my $balance = $self->charged;
363 $balance -= $_->amount foreach ( $self->cust_bill_pay );
364 $balance -= $_->amount foreach ( $self->cust_credited );
365 $balance = sprintf( "%.2f", $balance);
366 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
371 =item generate_email PARAMHASH
373 PARAMHASH can contain the following:
377 =item from => sender address, required
379 =item tempate => alternate template name, optional
381 =item print_text => text attachment arrayref, optional
383 =item subject => email subject, optional
387 Returns an argument list to be passed to L<FS::Misc::send_email>.
398 my $me = '[FS::cust_bill::generate_email]';
401 'from' => $args{'from'},
402 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
405 if (ref($args{'to'} eq 'ARRAY')) {
406 $return{'to'} = $args{'to'};
408 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
409 $self->cust_main->invoicing_list
413 if ( $conf->exists('invoice_html') ) {
415 warn "$me creating HTML/text multipart message"
418 $return{'nobody'} = 1;
420 my $alternative = build MIME::Entity
421 'Type' => 'multipart/alternative',
422 'Encoding' => '7bit',
423 'Disposition' => 'inline'
427 if ( $conf->exists('invoice_email_pdf')
428 and scalar($conf->config('invoice_email_pdf_note')) ) {
430 warn "$me using 'invoice_email_pdf_note' in multipart message"
432 $data = [ map { $_ . "\n" }
433 $conf->config('invoice_email_pdf_note')
438 warn "$me not using 'invoice_email_pdf_note' in multipart message"
440 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
441 $data = $args{'print_text'};
443 $data = [ $self->print_text('', $args{'template'}) ];
448 $alternative->attach(
449 'Type' => 'text/plain',
450 #'Encoding' => 'quoted-printable',
451 'Encoding' => '7bit',
453 'Disposition' => 'inline',
456 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
457 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
459 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
461 if ( defined($args{'_template'}) && length($args{'_template'})
462 && -e "$path/logo_". $args{'_template'}. ".png"
465 $file = "$path/logo_". $args{'_template'}. ".png";
467 $file = "$path/logo.png";
470 my $image = build MIME::Entity
471 'Type' => 'image/png',
472 'Encoding' => 'base64',
474 'Filename' => 'logo.png',
475 'Content-ID' => "<$content_id>",
478 $alternative->attach(
479 'Type' => 'text/html',
480 'Encoding' => 'quoted-printable',
481 'Data' => [ '<html>',
484 ' '. encode_entities($return{'subject'}),
487 ' <body bgcolor="#e8e8e8">',
488 $self->print_html('', $args{'template'}, $content_id),
492 'Disposition' => 'inline',
493 #'Filename' => 'invoice.pdf',
496 if ( $conf->exists('invoice_email_pdf') ) {
501 # multipart/alternative
507 my $related = build MIME::Entity 'Type' => 'multipart/related',
508 'Encoding' => '7bit';
510 #false laziness w/Misc::send_email
511 $related->head->replace('Content-type',
513 '; boundary="'. $related->head->multipart_boundary. '"'.
514 '; type=multipart/alternative'
517 $related->add_part($alternative);
519 $related->add_part($image);
521 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
523 $return{'mimeparts'} = [ $related, $pdf ];
527 #no other attachment:
529 # multipart/alternative
534 $return{'content-type'} = 'multipart/related';
535 $return{'mimeparts'} = [ $alternative, $image ];
536 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
537 #$return{'disposition'} = 'inline';
543 if ( $conf->exists('invoice_email_pdf') ) {
544 warn "$me creating PDF attachment"
547 #mime parts arguments a la MIME::Entity->build().
548 $return{'mimeparts'} = [
549 { $self->mimebuild_pdf('', $args{'template'}) }
553 if ( $conf->exists('invoice_email_pdf')
554 and scalar($conf->config('invoice_email_pdf_note')) ) {
556 warn "$me using 'invoice_email_pdf_note'"
558 $return{'body'} = [ map { $_ . "\n" }
559 $conf->config('invoice_email_pdf_note')
564 warn "$me not using 'invoice_email_pdf_note'"
566 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
567 $return{'body'} = $args{'print_text'};
569 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
582 Returns a list suitable for passing to MIME::Entity->build(), representing
583 this invoice as PDF attachment.
590 'Type' => 'application/pdf',
591 'Encoding' => 'base64',
592 'Data' => [ $self->print_pdf(@_) ],
593 'Disposition' => 'attachment',
594 'Filename' => 'invoice.pdf',
598 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
600 Sends this invoice to the destinations configured for this customer: sends
601 email, prints and/or faxes. See L<FS::cust_main_invoice>.
603 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
605 AGENTNUM, if specified, means that this invoice will only be sent for customers
606 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
607 single agent) or an arrayref of agentnums.
609 INVOICE_FROM, if specified, overrides the default email invoice From: address.
615 my $template = scalar(@_) ? shift : '';
616 if ( scalar(@_) && $_[0] ) {
617 my $agentnums = ref($_[0]) ? shift : [ shift ];
618 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
624 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
626 my @invoicing_list = $self->cust_main->invoicing_list;
628 $self->email($template, $invoice_from)
629 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
631 $self->print($template)
632 if grep { $_ eq 'POST' } @invoicing_list; #postal
634 $self->fax($template)
635 if grep { $_ eq 'FAX' } @invoicing_list; #fax
641 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
645 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
647 INVOICE_FROM, if specified, overrides the default email invoice From: address.
653 my $template = scalar(@_) ? shift : '';
657 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
659 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
660 $self->cust_main->invoicing_list;
662 #better to notify this person than silence
663 @invoicing_list = ($invoice_from) unless @invoicing_list;
665 my $error = send_email(
666 $self->generate_email(
667 'from' => $invoice_from,
668 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
669 'template' => $template,
672 die "can't email invoice: $error\n" if $error;
673 #die "$error\n" if $error;
677 =item lpr_data [ TEMPLATENAME ]
679 Returns the postscript or plaintext for this invoice as an arrayref.
681 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
686 my( $self, $template) = @_;
687 $conf->exists('invoice_latex')
688 ? [ $self->print_ps('', $template) ]
689 : [ $self->print_text('', $template) ];
692 =item print [ TEMPLATENAME ]
696 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
702 my $template = scalar(@_) ? shift : '';
704 my $lpr = $conf->config('lpr');
707 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
709 $outerr = ": $outerr" if length($outerr);
710 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
715 =item fax [ TEMPLATENAME ]
719 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
725 my $template = scalar(@_) ? shift : '';
727 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
728 unless $conf->exists('invoice_latex');
730 my $dialstring = $self->cust_main->getfield('fax');
733 my $error = send_fax( 'docdata' => $self->lpr_data($template),
734 'dialstring' => $dialstring,
736 die $error if $error;
740 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
742 Like B<send>, but only sends the invoice if it is the newest open invoice for
752 grep { $_->owed > 0 }
753 qsearch('cust_bill', {
754 'custnum' => $self->custnum,
755 #'_date' => { op=>'>', value=>$self->_date },
756 'invnum' => { op=>'>', value=>$self->invnum },
763 =item send_csv OPTION => VALUE, ...
765 Sends invoice as a CSV data-file to a remote host with the specified protocol.
769 protocol - currently only "ftp"
775 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
776 and YYMMDDHHMMSS is a timestamp.
778 See L</print_csv> for a description of the output format.
783 my($self, %opt) = @_;
787 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
788 mkdir $spooldir, 0700 unless -d $spooldir;
790 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
791 my $file = "$spooldir/$tracctnum.csv";
793 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
795 open(CSV, ">$file") or die "can't open $file: $!";
803 if ( $opt{protocol} eq 'ftp' ) {
804 eval "use Net::FTP;";
806 $net = Net::FTP->new($opt{server}) or die @$;
808 die "unknown protocol: $opt{protocol}";
811 $net->login( $opt{username}, $opt{password} )
812 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
814 $net->binary or die "can't set binary mode";
816 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
818 $net->put($file) or die "can't put $file: $!";
828 Spools CSV invoice data.
834 =item format - 'default' or 'billco'
836 =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>).
838 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
840 =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.
847 my($self, %opt) = @_;
849 my $cust_main = $self->cust_main;
851 if ( $opt{'dest'} ) {
852 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
853 $cust_main->invoicing_list;
854 return 'N/A' unless $invoicing_list{$opt{'dest'}}
855 || ! keys %invoicing_list;
858 if ( $opt{'balanceover'} ) {
860 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
863 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
864 mkdir $spooldir, 0700 unless -d $spooldir;
866 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
870 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
871 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
874 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
876 open(CSV, ">>$file") or die "can't open $file: $!";
882 if ( lc($opt{'format'}) eq 'billco' ) {
889 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
892 open(CSV,">>$file") or die "can't open $file: $!";
906 =item print_csv OPTION => VALUE, ...
908 Returns CSV data for this invoice.
912 format - 'default' or 'billco'
914 Returns a list consisting of two scalars. The first is a single line of CSV
915 header information for this invoice. The second is one or more lines of CSV
916 detail information for this invoice.
918 If I<format> is not specified or "default", the fields of the CSV file are as
921 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
925 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
927 B<record_type> is C<cust_bill> for the initial header line only. The
928 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
929 fields are filled in.
931 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
932 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
935 =item invnum - invoice number
937 =item custnum - customer number
939 =item _date - invoice date
941 =item charged - total invoice amount
943 =item first - customer first name
945 =item last - customer first name
947 =item company - company name
949 =item address1 - address line 1
951 =item address2 - address line 1
961 =item pkg - line item description
963 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
965 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
967 =item sdate - start date for recurring fee
969 =item edate - end date for recurring fee
973 If I<format> is "billco", the fields of the header CSV file are as follows:
975 +-------------------------------------------------------------------+
976 | FORMAT HEADER FILE |
977 |-------------------------------------------------------------------|
978 | Field | Description | Name | Type | Width |
979 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
980 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
981 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
982 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
983 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
984 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
985 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
986 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
987 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
988 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
989 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
990 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
991 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
992 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
993 | 15 | Previous Balance | BALFWD | NUM* | 9 |
994 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
995 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
996 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
997 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
998 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
999 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1000 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1001 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1002 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1003 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1004 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1005 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1006 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1007 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1008 +-------+-------------------------------+------------+------+-------+
1010 If I<format> is "billco", the fields of the detail CSV file are as follows:
1012 FORMAT FOR DETAIL FILE
1014 Field | Description | Name | Type | Width
1015 1 | N/A-Leave Empty | RC | CHAR | 2
1016 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1017 3 | Account Number | TRACCTNUM | CHAR | 15
1018 4 | Invoice Number | TRINVOICE | CHAR | 15
1019 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1020 6 | Transaction Detail | DETAILS | CHAR | 100
1021 7 | Amount | AMT | NUM* | 9
1022 8 | Line Format Control** | LNCTRL | CHAR | 2
1023 9 | Grouping Code | GROUP | CHAR | 2
1024 10 | User Defined | ACCT CODE | CHAR | 15
1029 my($self, %opt) = @_;
1031 eval "use Text::CSV_XS";
1034 my $cust_main = $self->cust_main;
1036 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1038 if ( lc($opt{'format'}) eq 'billco' ) {
1041 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1044 if ( $conf->exists('invoice_default_terms')
1045 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1046 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1049 my( $previous_balance, @unused ) = $self->previous; #previous balance
1051 my $pmt_cr_applied = 0;
1052 $pmt_cr_applied += $_->{'amount'}
1053 foreach ( $self->_items_payments, $self->_items_credits ) ;
1055 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1058 '', # 1 | N/A-Leave Empty CHAR 2
1059 '', # 2 | N/A-Leave Empty CHAR 15
1060 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1061 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1062 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1063 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1064 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1065 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1066 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1067 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1068 '', # 10 | Ancillary Billing Information CHAR 30
1069 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1070 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1073 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1076 $duedate, # 14 | Bill Due Date CHAR 10
1078 $previous_balance, # 15 | Previous Balance NUM* 9
1079 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1080 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1081 $totaldue, # 18 | Total Amt Due NUM* 9
1082 $totaldue, # 19 | Total Amt Due NUM* 9
1083 '', # 20 | 30 Day Aging NUM* 9
1084 '', # 21 | 60 Day Aging NUM* 9
1085 '', # 22 | 90 Day Aging NUM* 9
1086 'N', # 23 | Y/N CHAR 1
1087 '', # 24 | Remittance automation CHAR 100
1088 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1089 $self->custnum, # 26 | Customer Reference Number CHAR 15
1090 '0', # 27 | Federal Tax*** NUM* 9
1091 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1092 '0', # 29 | Other Taxes & Fees*** NUM* 9
1101 time2str("%x", $self->_date),
1102 sprintf("%.2f", $self->charged),
1103 ( map { $cust_main->getfield($_) }
1104 qw( first last company address1 address2 city state zip country ) ),
1106 ) or die "can't create csv";
1109 my $header = $csv->string. "\n";
1112 if ( lc($opt{'format'}) eq 'billco' ) {
1115 foreach my $item ( $self->_items_pkg ) {
1118 '', # 1 | N/A-Leave Empty CHAR 2
1119 '', # 2 | N/A-Leave Empty CHAR 15
1120 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1121 $self->invnum, # 4 | Invoice Number CHAR 15
1122 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1123 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1124 $item->{'amount'}, # 7 | Amount NUM* 9
1125 '', # 8 | Line Format Control** CHAR 2
1126 '', # 9 | Grouping Code CHAR 2
1127 '', # 10 | User Defined CHAR 15
1130 $detail .= $csv->string. "\n";
1136 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1138 my($pkg, $setup, $recur, $sdate, $edate);
1139 if ( $cust_bill_pkg->pkgnum ) {
1141 ($pkg, $setup, $recur, $sdate, $edate) = (
1142 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1143 ( $cust_bill_pkg->setup != 0
1144 ? sprintf("%.2f", $cust_bill_pkg->setup )
1146 ( $cust_bill_pkg->recur != 0
1147 ? sprintf("%.2f", $cust_bill_pkg->recur )
1149 ( $cust_bill_pkg->sdate
1150 ? time2str("%x", $cust_bill_pkg->sdate)
1152 ($cust_bill_pkg->edate
1153 ?time2str("%x", $cust_bill_pkg->edate)
1157 } else { #pkgnum tax
1158 next unless $cust_bill_pkg->setup != 0;
1159 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1160 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1162 ($pkg, $setup, $recur, $sdate, $edate) =
1163 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1169 ( map { '' } (1..11) ),
1170 ($pkg, $setup, $recur, $sdate, $edate)
1171 ) or die "can't create csv";
1173 $detail .= $csv->string. "\n";
1179 ( $header, $detail );
1185 Pays this invoice with a compliemntary payment. If there is an error,
1186 returns the error, otherwise returns false.
1192 my $cust_pay = new FS::cust_pay ( {
1193 'invnum' => $self->invnum,
1194 'paid' => $self->owed,
1197 'payinfo' => $self->cust_main->payinfo,
1205 Attempts to pay this invoice with a credit card payment via a
1206 Business::OnlinePayment realtime gateway. See
1207 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1208 for supported processors.
1214 $self->realtime_bop( 'CC', @_ );
1219 Attempts to pay this invoice with an electronic check (ACH) payment via a
1220 Business::OnlinePayment realtime gateway. See
1221 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1222 for supported processors.
1228 $self->realtime_bop( 'ECHECK', @_ );
1233 Attempts to pay this invoice with phone bill (LEC) payment via a
1234 Business::OnlinePayment realtime gateway. See
1235 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1236 for supported processors.
1242 $self->realtime_bop( 'LEC', @_ );
1246 my( $self, $method ) = @_;
1248 my $cust_main = $self->cust_main;
1249 my $balance = $cust_main->balance;
1250 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1251 $amount = sprintf("%.2f", $amount);
1252 return "not run (balance $balance)" unless $amount > 0;
1254 my $description = 'Internet Services';
1255 if ( $conf->exists('business-onlinepayment-description') ) {
1256 my $dtempl = $conf->config('business-onlinepayment-description');
1258 my $agent_obj = $cust_main->agent
1259 or die "can't retreive agent for $cust_main (agentnum ".
1260 $cust_main->agentnum. ")";
1261 my $agent = $agent_obj->agent;
1262 my $pkgs = join(', ',
1263 map { $_->cust_pkg->part_pkg->pkg }
1264 grep { $_->pkgnum } $self->cust_bill_pkg
1266 $description = eval qq("$dtempl");
1269 $cust_main->realtime_bop($method, $amount,
1270 'description' => $description,
1271 'invnum' => $self->invnum,
1278 Adds a payment for this invoice to the pending credit card batch (see
1279 L<FS::cust_pay_batch>).
1285 my $cust_main = $self->cust_main;
1287 my $oldAutoCommit = $FS::UID::AutoCommit;
1288 local $FS::UID::AutoCommit = 0;
1291 my $pay_batch = qsearchs('pay_batch', {'status' => 'O'});
1293 unless ( $pay_batch ) {
1294 $pay_batch = new FS::pay_batch;
1295 $pay_batch->setfield('status' => 'O');
1296 my $error = $pay_batch->insert;
1298 $dbh->rollback if $oldAutoCommit;
1299 die "error creating new batch: $error\n";
1303 my $cust_pay_batch = new FS::cust_pay_batch ( {
1304 'batchnum' => $pay_batch->getfield('batchnum'),
1305 'invnum' => $self->getfield('invnum'),
1306 'custnum' => $cust_main->getfield('custnum'),
1307 'last' => $cust_main->getfield('last'),
1308 'first' => $cust_main->getfield('first'),
1309 'address1' => $cust_main->getfield('address1'),
1310 'address2' => $cust_main->getfield('address2'),
1311 'city' => $cust_main->getfield('city'),
1312 'state' => $cust_main->getfield('state'),
1313 'zip' => $cust_main->getfield('zip'),
1314 'country' => $cust_main->getfield('country'),
1315 'payinfo' => $cust_main->payinfo,
1316 'exp' => $cust_main->getfield('paydate'),
1317 'payname' => $cust_main->getfield('payname'),
1318 'amount' => $self->owed,
1320 my $error = $cust_pay_batch->insert;
1322 $dbh->rollback if $oldAutoCommit;
1326 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1330 sub _agent_template {
1332 $self->_agent_plandata('agent_templatename');
1335 sub _agent_invoice_from {
1337 $self->_agent_plandata('agent_invoice_from');
1340 sub _agent_plandata {
1341 my( $self, $option ) = @_;
1343 my $part_bill_event = qsearchs( 'part_bill_event',
1345 'payby' => $self->cust_main->payby,
1346 'plan' => 'send_agent',
1347 'plandata' => { 'op' => '~',
1348 'value' => "(^|\n)agentnum ".
1350 $self->cust_main->agentnum.
1356 'ORDER BY seconds LIMIT 1'
1359 return '' unless $part_bill_event;
1361 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1364 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1365 " plandata for $option";
1371 =item print_text [ TIME [ , TEMPLATE ] ]
1373 Returns an text invoice, as a list of lines.
1375 TIME an optional value used to control the printing of overdue messages. The
1376 default is now. It isn't the date of the invoice; that's the `_date' field.
1377 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1378 L<Time::Local> and L<Date::Parse> for conversion functions.
1382 #still some false laziness w/_items stuff (and send_csv)
1385 my( $self, $today, $template ) = @_;
1388 # my $invnum = $self->invnum;
1389 my $cust_main = $self->cust_main;
1390 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1391 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1393 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1394 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1395 #my $balance_due = $self->owed + $pr_total - $cr_total;
1396 my $balance_due = $self->owed + $pr_total;
1399 #my($description,$amount);
1403 foreach ( @pr_cust_bill ) {
1405 "Previous Balance, Invoice #". $_->invnum.
1406 " (". time2str("%x",$_->_date). ")",
1407 $money_char. sprintf("%10.2f",$_->owed)
1410 if (@pr_cust_bill) {
1411 push @buf,['','-----------'];
1412 push @buf,[ 'Total Previous Balance',
1413 $money_char. sprintf("%10.2f",$pr_total ) ];
1418 foreach my $cust_bill_pkg (
1419 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1420 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1423 my $desc = $cust_bill_pkg->desc;
1425 if ( $cust_bill_pkg->pkgnum > 0 ) {
1427 if ( $cust_bill_pkg->setup != 0 ) {
1428 my $description = $desc;
1429 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1430 push @buf, [ $description,
1431 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1433 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1434 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1437 if ( $cust_bill_pkg->recur != 0 ) {
1439 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1440 time2str("%x", $cust_bill_pkg->edate) . ")",
1441 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1444 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1445 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1446 $cust_bill_pkg->sdate );
1449 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1451 } else { #pkgnum tax or one-shot line item
1453 if ( $cust_bill_pkg->setup != 0 ) {
1455 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1457 if ( $cust_bill_pkg->recur != 0 ) {
1458 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1459 . time2str("%x", $cust_bill_pkg->edate). ")",
1460 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1468 push @buf,['','-----------'];
1469 push @buf,['Total New Charges',
1470 $money_char. sprintf("%10.2f",$self->charged) ];
1473 push @buf,['','-----------'];
1474 push @buf,['Total Charges',
1475 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1479 foreach ( $self->cust_credited ) {
1481 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1483 my $reason = substr($_->cust_credit->reason,0,32);
1484 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1485 $reason = " ($reason) " if $reason;
1487 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1489 $money_char. sprintf("%10.2f",$_->amount)
1492 #foreach ( @cr_cust_credit ) {
1494 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1495 # $money_char. sprintf("%10.2f",$_->credited)
1499 #get & print payments
1500 foreach ( $self->cust_bill_pay ) {
1502 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1505 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1506 $money_char. sprintf("%10.2f",$_->amount )
1511 my $balance_due_msg = $self->balance_due_msg;
1513 push @buf,['','-----------'];
1514 push @buf,[$balance_due_msg, $money_char.
1515 sprintf("%10.2f", $balance_due ) ];
1517 #create the template
1518 $template ||= $self->_agent_template;
1519 my $templatefile = 'invoice_template';
1520 $templatefile .= "_$template" if length($template);
1521 my @invoice_template = $conf->config($templatefile)
1522 or die "cannot load config file $templatefile";
1525 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1526 /invoice_lines\((\d*)\)/;
1527 $invoice_lines += $1 || scalar(@buf);
1530 die "no invoice_lines() functions in template?" unless $wasfunc;
1531 my $invoice_template = new Text::Template (
1533 SOURCE => [ map "$_\n", @invoice_template ],
1534 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1535 $invoice_template->compile()
1536 or die "can't compile template: $Text::Template::ERROR";
1538 #setup template variables
1539 package FS::cust_bill::_template; #!
1540 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1542 $invnum = $self->invnum;
1543 $date = $self->_date;
1545 $agent = $self->cust_main->agent->agent;
1547 if ( $FS::cust_bill::invoice_lines ) {
1549 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1551 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1556 #format address (variable for the template)
1558 @address = ( '', '', '', '', '', '' );
1559 package FS::cust_bill; #!
1560 $FS::cust_bill::_template::address[$l++] =
1561 $cust_main->payname.
1562 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1563 ? " (P.O. #". $cust_main->payinfo. ")"
1567 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1568 if $cust_main->company;
1569 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1570 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1571 if $cust_main->address2;
1572 $FS::cust_bill::_template::address[$l++] =
1573 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1575 my $countrydefault = $conf->config('countrydefault') || 'US';
1576 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1577 unless $cust_main->country eq $countrydefault;
1579 # #overdue? (variable for the template)
1580 # $FS::cust_bill::_template::overdue = (
1582 # && $today > $self->_date
1583 ## && $self->printed > 1
1584 # && $self->printed > 0
1587 #and subroutine for the template
1588 sub FS::cust_bill::_template::invoice_lines {
1589 my $lines = shift || scalar(@buf);
1591 scalar(@buf) ? shift @buf : [ '', '' ];
1597 $FS::cust_bill::_template::page = 1;
1601 push @collect, split("\n",
1602 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1604 $FS::cust_bill::_template::page++;
1607 map "$_\n", @collect;
1611 =item print_latex [ TIME [ , TEMPLATE ] ]
1613 Internal method - returns a filename of a filled-in LaTeX template for this
1614 invoice (Note: add ".tex" to get the actual filename).
1616 See print_ps and print_pdf for methods that return PostScript and PDF output.
1618 TIME an optional value used to control the printing of overdue messages. The
1619 default is now. It isn't the date of the invoice; that's the `_date' field.
1620 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1621 L<Time::Local> and L<Date::Parse> for conversion functions.
1625 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1628 my( $self, $today, $template ) = @_;
1630 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1633 my $cust_main = $self->cust_main;
1634 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1635 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1637 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1638 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1639 #my $balance_due = $self->owed + $pr_total - $cr_total;
1640 my $balance_due = $self->owed + $pr_total;
1642 #create the template
1643 $template ||= $self->_agent_template;
1644 my $templatefile = 'invoice_latex';
1645 my $suffix = length($template) ? "_$template" : '';
1646 $templatefile .= $suffix;
1647 my @invoice_template = map "$_\n", $conf->config($templatefile)
1648 or die "cannot load config file $templatefile";
1650 my($format, $text_template);
1651 if ( grep { /^%%Detail/ } @invoice_template ) {
1652 #change this to a die when the old code is removed
1653 warn "old-style invoice template $templatefile; ".
1654 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1657 $format = 'Text::Template';
1658 $text_template = new Text::Template(
1660 SOURCE => \@invoice_template,
1661 DELIMITERS => [ '[@--', '--@]' ],
1664 $text_template->compile()
1665 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1669 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1670 $returnaddress = join("\n",
1671 $conf->config_orbase('invoice_latexreturnaddress', $template)
1674 $returnaddress = '~';
1677 my %invoice_data = (
1678 'invnum' => $self->invnum,
1679 'date' => time2str('%b %o, %Y', $self->_date),
1680 'today' => time2str('%b %o, %Y', $today),
1681 'agent' => _latex_escape($cust_main->agent->agent),
1682 'payname' => _latex_escape($cust_main->payname),
1683 'company' => _latex_escape($cust_main->company),
1684 'address1' => _latex_escape($cust_main->address1),
1685 'address2' => _latex_escape($cust_main->address2),
1686 'city' => _latex_escape($cust_main->city),
1687 'state' => _latex_escape($cust_main->state),
1688 'zip' => _latex_escape($cust_main->zip),
1689 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1690 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1691 'returnaddress' => $returnaddress,
1693 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1694 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1695 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1698 my $countrydefault = $conf->config('countrydefault') || 'US';
1699 if ( $cust_main->country eq $countrydefault ) {
1700 $invoice_data{'country'} = '';
1702 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1705 $invoice_data{'notes'} =
1707 # #do variable substitutions in notes
1708 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1709 $conf->config_orbase('invoice_latexnotes', $template)
1711 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1714 $invoice_data{'footer'} =~ s/\n+$//;
1715 $invoice_data{'smallfooter'} =~ s/\n+$//;
1716 $invoice_data{'notes'} =~ s/\n+$//;
1718 $invoice_data{'po_line'} =
1719 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1720 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1724 if ( $format eq 'old' ) {
1727 my @total_item = ();
1728 while ( @invoice_template ) {
1729 my $line = shift @invoice_template;
1731 if ( $line =~ /^%%Detail\s*$/ ) {
1733 while ( ( my $line_item_line = shift @invoice_template )
1734 !~ /^%%EndDetail\s*$/ ) {
1735 push @line_item, $line_item_line;
1737 foreach my $line_item ( $self->_items ) {
1738 #foreach my $line_item ( $self->_items_pkg ) {
1739 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1740 $invoice_data{'description'} =
1741 _latex_escape($line_item->{'description'});
1742 if ( exists $line_item->{'ext_description'} ) {
1743 $invoice_data{'description'} .=
1744 "\\tabularnewline\n~~".
1745 join( "\\tabularnewline\n~~",
1746 map _latex_escape($_), @{$line_item->{'ext_description'}}
1749 $invoice_data{'amount'} = $line_item->{'amount'};
1750 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1752 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1755 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1757 while ( ( my $total_item_line = shift @invoice_template )
1758 !~ /^%%EndTotalDetails\s*$/ ) {
1759 push @total_item, $total_item_line;
1762 my @total_fill = ();
1765 foreach my $tax ( $self->_items_tax ) {
1766 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1767 $taxtotal += $tax->{'amount'};
1768 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1770 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1775 $invoice_data{'total_item'} = 'Sub-total';
1776 $invoice_data{'total_amount'} =
1777 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1778 unshift @total_fill,
1779 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1783 $invoice_data{'total_item'} = '\textbf{Total}';
1784 $invoice_data{'total_amount'} =
1785 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1787 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1790 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1793 foreach my $credit ( $self->_items_credits ) {
1794 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1796 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1798 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1803 foreach my $payment ( $self->_items_payments ) {
1804 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1806 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1808 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1812 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1813 $invoice_data{'total_amount'} =
1814 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1816 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1819 push @filled_in, @total_fill;
1822 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1823 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1824 push @filled_in, $line;
1835 } elsif ( $format eq 'Text::Template' ) {
1837 my @detail_items = ();
1838 my @total_items = ();
1840 $invoice_data{'detail_items'} = \@detail_items;
1841 $invoice_data{'total_items'} = \@total_items;
1843 foreach my $line_item ( $self->_items ) {
1845 ext_description => [],
1847 $detail->{'ref'} = $line_item->{'pkgnum'};
1848 $detail->{'quantity'} = 1;
1849 $detail->{'description'} = _latex_escape($line_item->{'description'});
1850 if ( exists $line_item->{'ext_description'} ) {
1851 @{$detail->{'ext_description'}} = map {
1853 } @{$line_item->{'ext_description'}};
1855 $detail->{'amount'} = $line_item->{'amount'};
1856 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1858 push @detail_items, $detail;
1863 foreach my $tax ( $self->_items_tax ) {
1865 $total->{'total_item'} = _latex_escape($tax->{'description'});
1866 $taxtotal += $tax->{'amount'};
1867 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1868 push @total_items, $total;
1873 $total->{'total_item'} = 'Sub-total';
1874 $total->{'total_amount'} =
1875 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1876 unshift @total_items, $total;
1881 $total->{'total_item'} = '\textbf{Total}';
1882 $total->{'total_amount'} =
1883 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1884 push @total_items, $total;
1887 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1890 foreach my $credit ( $self->_items_credits ) {
1892 $total->{'total_item'} = _latex_escape($credit->{'description'});
1894 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1895 push @total_items, $total;
1899 foreach my $payment ( $self->_items_payments ) {
1901 $total->{'total_item'} = _latex_escape($payment->{'description'});
1903 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1904 push @total_items, $total;
1909 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1910 $total->{'total_amount'} =
1911 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1912 push @total_items, $total;
1916 die "guru meditation #54";
1919 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1920 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1924 ) or die "can't open temp file: $!\n";
1925 if ( $format eq 'old' ) {
1926 print $fh join('', @filled_in );
1927 } elsif ( $format eq 'Text::Template' ) {
1928 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1930 die "guru meditation #32";
1934 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1939 =item print_ps [ TIME [ , TEMPLATE ] ]
1941 Returns an postscript invoice, as a scalar.
1943 TIME an optional value used to control the printing of overdue messages. The
1944 default is now. It isn't the date of the invoice; that's the `_date' field.
1945 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1946 L<Time::Local> and L<Date::Parse> for conversion functions.
1953 my $file = $self->print_latex(@_);
1955 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1958 my $sfile = shell_quote $file;
1960 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1961 or die "pslatex $file.tex failed; see $file.log for details?\n";
1962 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1963 or die "pslatex $file.tex failed; see $file.log for details?\n";
1965 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1966 or die "dvips failed";
1968 open(POSTSCRIPT, "<$file.ps")
1969 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1971 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1974 while (<POSTSCRIPT>) {
1984 =item print_pdf [ TIME [ , TEMPLATE ] ]
1986 Returns an PDF invoice, as a scalar.
1988 TIME an optional value used to control the printing of overdue messages. The
1989 default is now. It isn't the date of the invoice; that's the `_date' field.
1990 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1991 L<Time::Local> and L<Date::Parse> for conversion functions.
1998 my $file = $self->print_latex(@_);
2000 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2003 #system('pdflatex', "$file.tex");
2004 #system('pdflatex', "$file.tex");
2005 #! LaTeX Error: Unknown graphics extension: .eps.
2007 my $sfile = shell_quote $file;
2009 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2010 or die "pslatex $file.tex failed; see $file.log for details?\n";
2011 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2012 or die "pslatex $file.tex failed; see $file.log for details?\n";
2014 #system('dvipdf', "$file.dvi", "$file.pdf" );
2016 "dvips -q -t letter -f $sfile.dvi ".
2017 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2020 or die "dvips | gs failed: $!";
2022 open(PDF, "<$file.pdf")
2023 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2025 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2038 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2040 Returns an HTML invoice, as a scalar.
2042 TIME an optional value used to control the printing of overdue messages. The
2043 default is now. It isn't the date of the invoice; that's the `_date' field.
2044 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2045 L<Time::Local> and L<Date::Parse> for conversion functions.
2047 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2048 when emailing the invoice as part of a multipart/related MIME email.
2052 #some falze laziness w/print_text and print_latex (and send_csv)
2054 my( $self, $today, $template, $cid ) = @_;
2057 my $cust_main = $self->cust_main;
2058 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2059 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2061 $template ||= $self->_agent_template;
2062 my $templatefile = 'invoice_html';
2063 my $suffix = length($template) ? "_$template" : '';
2064 $templatefile .= $suffix;
2065 my @html_template = map "$_\n", $conf->config($templatefile)
2066 or die "cannot load config file $templatefile";
2068 my $html_template = new Text::Template(
2070 SOURCE => \@html_template,
2071 DELIMITERS => [ '<%=', '%>' ],
2074 $html_template->compile()
2075 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2077 my %invoice_data = (
2078 'invnum' => $self->invnum,
2079 'date' => time2str('%b %o, %Y', $self->_date),
2080 'today' => time2str('%b %o, %Y', $today),
2081 'agent' => encode_entities($cust_main->agent->agent),
2082 'payname' => encode_entities($cust_main->payname),
2083 'company' => encode_entities($cust_main->company),
2084 'address1' => encode_entities($cust_main->address1),
2085 'address2' => encode_entities($cust_main->address2),
2086 'city' => encode_entities($cust_main->city),
2087 'state' => encode_entities($cust_main->state),
2088 'zip' => encode_entities($cust_main->zip),
2089 'terms' => $conf->config('invoice_default_terms')
2090 || 'Payable upon receipt',
2092 'template' => $template,
2093 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2097 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2098 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2100 $invoice_data{'returnaddress'} =
2101 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2103 $invoice_data{'returnaddress'} =
2106 s/\\\\\*?\s*$/<BR>/;
2107 s/\\hyphenation\{[\w\s\-]+\}//;
2110 $conf->config_orbase( 'invoice_latexreturnaddress',
2116 my $countrydefault = $conf->config('countrydefault') || 'US';
2117 if ( $cust_main->country eq $countrydefault ) {
2118 $invoice_data{'country'} = '';
2120 $invoice_data{'country'} =
2121 encode_entities(code2country($cust_main->country));
2125 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2126 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2128 $invoice_data{'notes'} =
2129 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2131 $invoice_data{'notes'} =
2133 s/%%(.*)$/<!-- $1 -->/;
2134 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2135 s/\\begin\{enumerate\}/<ol>/;
2137 s/\\end\{enumerate\}/<\/ol>/;
2138 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2141 $conf->config_orbase('invoice_latexnotes', $template)
2145 # #do variable substitutions in notes
2146 # $invoice_data{'notes'} =
2148 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2149 # $conf->config_orbase('invoice_latexnotes', $suffix)
2153 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2154 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2156 $invoice_data{'footer'} =
2157 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2159 $invoice_data{'footer'} =
2160 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2161 $conf->config_orbase('invoice_latexfooter', $template)
2165 $invoice_data{'po_line'} =
2166 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2167 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2170 my $money_char = $conf->config('money_char') || '$';
2172 foreach my $line_item ( $self->_items ) {
2174 ext_description => [],
2176 $detail->{'ref'} = $line_item->{'pkgnum'};
2177 $detail->{'description'} = encode_entities($line_item->{'description'});
2178 if ( exists $line_item->{'ext_description'} ) {
2179 @{$detail->{'ext_description'}} = map {
2180 encode_entities($_);
2181 } @{$line_item->{'ext_description'}};
2183 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2184 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2186 push @{$invoice_data{'detail_items'}}, $detail;
2191 foreach my $tax ( $self->_items_tax ) {
2193 $total->{'total_item'} = encode_entities($tax->{'description'});
2194 $taxtotal += $tax->{'amount'};
2195 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2196 push @{$invoice_data{'total_items'}}, $total;
2201 $total->{'total_item'} = 'Sub-total';
2202 $total->{'total_amount'} =
2203 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2204 unshift @{$invoice_data{'total_items'}}, $total;
2207 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2210 $total->{'total_item'} = '<b>Total</b>';
2211 $total->{'total_amount'} =
2212 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2213 push @{$invoice_data{'total_items'}}, $total;
2216 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2219 foreach my $credit ( $self->_items_credits ) {
2221 $total->{'total_item'} = encode_entities($credit->{'description'});
2223 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2224 push @{$invoice_data{'total_items'}}, $total;
2228 foreach my $payment ( $self->_items_payments ) {
2230 $total->{'total_item'} = encode_entities($payment->{'description'});
2232 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2233 push @{$invoice_data{'total_items'}}, $total;
2238 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2239 $total->{'total_amount'} =
2240 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2241 push @{$invoice_data{'total_items'}}, $total;
2244 $html_template->fill_in( HASH => \%invoice_data);
2247 # quick subroutine for print_latex
2249 # There are ten characters that LaTeX treats as special characters, which
2250 # means that they do not simply typeset themselves:
2251 # # $ % & ~ _ ^ \ { }
2253 # TeX ignores blanks following an escaped character; if you want a blank (as
2254 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2258 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2259 $value =~ s/([<>])/\$$1\$/g;
2263 #utility methods for print_*
2265 sub balance_due_msg {
2267 my $msg = 'Balance Due';
2268 return $msg unless $conf->exists('invoice_default_terms');
2269 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2270 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2271 } elsif ( $conf->config('invoice_default_terms') ) {
2272 $msg .= ' - '. $conf->config('invoice_default_terms');
2279 my @display = scalar(@_)
2281 : qw( _items_previous _items_pkg );
2282 #: qw( _items_pkg );
2283 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2285 foreach my $display ( @display ) {
2286 push @b, $self->$display(@_);
2291 sub _items_previous {
2293 my $cust_main = $self->cust_main;
2294 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2296 foreach ( @pr_cust_bill ) {
2298 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2299 ' ('. time2str('%x',$_->_date). ')',
2300 #'pkgpart' => 'N/A',
2302 'amount' => sprintf("%.2f", $_->owed),
2308 # 'description' => 'Previous Balance',
2309 # #'pkgpart' => 'N/A',
2310 # 'pkgnum' => 'N/A',
2311 # 'amount' => sprintf("%10.2f", $pr_total ),
2312 # 'ext_description' => [ map {
2313 # "Invoice ". $_->invnum.
2314 # " (". time2str("%x",$_->_date). ") ".
2315 # sprintf("%10.2f", $_->owed)
2316 # } @pr_cust_bill ],
2323 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2324 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2329 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2330 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2333 sub _items_cust_bill_pkg {
2335 my $cust_bill_pkg = shift;
2338 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2340 my $desc = $cust_bill_pkg->desc;
2342 if ( $cust_bill_pkg->pkgnum > 0 ) {
2344 if ( $cust_bill_pkg->setup != 0 ) {
2345 my $description = $desc;
2346 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2347 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2348 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2350 description => $description,
2351 #pkgpart => $part_pkg->pkgpart,
2352 pkgnum => $cust_bill_pkg->pkgnum,
2353 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2354 ext_description => \@d,
2358 if ( $cust_bill_pkg->recur != 0 ) {
2360 description => "$desc (" .
2361 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2362 time2str('%x', $cust_bill_pkg->edate). ')',
2363 #pkgpart => $part_pkg->pkgpart,
2364 pkgnum => $cust_bill_pkg->pkgnum,
2365 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2367 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2368 $cust_bill_pkg->sdate),
2369 $cust_bill_pkg->details,
2374 } else { #pkgnum tax or one-shot line item (??)
2376 if ( $cust_bill_pkg->setup != 0 ) {
2378 'description' => $desc,
2379 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2382 if ( $cust_bill_pkg->recur != 0 ) {
2384 'description' => "$desc (".
2385 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2386 time2str("%x", $cust_bill_pkg->edate). ')',
2387 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2399 sub _items_credits {
2404 foreach ( $self->cust_credited ) {
2406 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2408 my $reason = $_->cust_credit->reason;
2409 #my $reason = substr($_->cust_credit->reason,0,32);
2410 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2411 $reason = " ($reason) " if $reason;
2413 #'description' => 'Credit ref\#'. $_->crednum.
2414 # " (". time2str("%x",$_->cust_credit->_date) .")".
2416 'description' => 'Credit applied '.
2417 time2str("%x",$_->cust_credit->_date). $reason,
2418 'amount' => sprintf("%.2f",$_->amount),
2421 #foreach ( @cr_cust_credit ) {
2423 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2424 # $money_char. sprintf("%10.2f",$_->credited)
2432 sub _items_payments {
2436 #get & print payments
2437 foreach ( $self->cust_bill_pay ) {
2439 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2442 'description' => "Payment received ".
2443 time2str("%x",$_->cust_pay->_date ),
2444 'amount' => sprintf("%.2f", $_->amount )
2463 sub process_reprint {
2464 process_re_X('print', @_);
2471 sub process_reemail {
2472 process_re_X('email', @_);
2480 process_re_X('fax', @_);
2483 use Storable qw(thaw);
2487 my( $method, $job ) = ( shift, shift );
2488 warn "process_re_X $method for job $job\n" if $DEBUG;
2490 my $param = thaw(decode_base64(shift));
2491 warn Dumper($param) if $DEBUG;
2502 my($method, $job, %param ) = @_;
2503 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2505 warn "re_X $method for job $job with param:\n".
2506 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2509 #some false laziness w/search/cust_bill.html
2511 my $orderby = 'ORDER BY cust_bill._date';
2515 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2516 push @where, "cust_bill._date >= $1";
2518 if ( $param{'end'} =~ /^(\d+)$/ ) {
2519 push @where, "cust_bill._date < $1";
2521 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2522 push @where, "cust_main.agentnum = $1";
2526 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2527 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2528 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2529 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2531 push @where, "0 != $owed"
2534 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2537 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2539 my $addl_from = 'left join cust_main using ( custnum )';
2541 if ( $param{'newest_percust'} ) {
2542 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2543 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2544 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2547 my @cust_bill = qsearch( 'cust_bill',
2549 "$distinct cust_bill.*",
2555 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2556 foreach my $cust_bill ( @cust_bill ) {
2557 $cust_bill->$method();
2559 if ( $job ) { #progressbar foo
2561 if ( time - $min_sec > $last ) {
2562 my $error = $job->update_statustext(
2563 int( 100 * $num / scalar(@cust_bill) )
2565 die $error if $error;
2580 print_text formatting (and some logic :/) is in source, but needs to be
2581 slurped in from a file. Also number of lines ($=).
2585 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2586 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base