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', {'status' => 'O'});
1291 unless ($pay_batch) {
1292 $pay_batch = new FS::pay_batch;
1293 $pay_batch->setfield('status' => 'O');
1294 my $error = $pay_batch->insert;
1296 die "error creating new batch: $error\n";
1300 my $cust_pay_batch = new FS::cust_pay_batch ( {
1301 'batchnum' => $pay_batch->getfield('batchnum'),
1302 'invnum' => $self->getfield('invnum'),
1303 'custnum' => $cust_main->getfield('custnum'),
1304 'last' => $cust_main->getfield('last'),
1305 'first' => $cust_main->getfield('first'),
1306 'address1' => $cust_main->getfield('address1'),
1307 'address2' => $cust_main->getfield('address2'),
1308 'city' => $cust_main->getfield('city'),
1309 'state' => $cust_main->getfield('state'),
1310 'zip' => $cust_main->getfield('zip'),
1311 'country' => $cust_main->getfield('country'),
1312 'payinfo' => $cust_main->payinfo,
1313 'exp' => $cust_main->getfield('paydate'),
1314 'payname' => $cust_main->getfield('payname'),
1315 'amount' => $self->owed,
1317 my $error = $cust_pay_batch->insert;
1318 die $error if $error;
1320 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1325 sub _agent_template {
1327 $self->_agent_plandata('agent_templatename');
1330 sub _agent_invoice_from {
1332 $self->_agent_plandata('agent_invoice_from');
1335 sub _agent_plandata {
1336 my( $self, $option ) = @_;
1338 my $part_bill_event = qsearchs( 'part_bill_event',
1340 'payby' => $self->cust_main->payby,
1341 'plan' => 'send_agent',
1342 'plandata' => { 'op' => '~',
1343 'value' => "(^|\n)agentnum ".
1345 $self->cust_main->agentnum.
1351 'ORDER BY seconds LIMIT 1'
1354 return '' unless $part_bill_event;
1356 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1359 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1360 " plandata for $option";
1366 =item print_text [ TIME [ , TEMPLATE ] ]
1368 Returns an text invoice, as a list of lines.
1370 TIME an optional value used to control the printing of overdue messages. The
1371 default is now. It isn't the date of the invoice; that's the `_date' field.
1372 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1373 L<Time::Local> and L<Date::Parse> for conversion functions.
1377 #still some false laziness w/_items stuff (and send_csv)
1380 my( $self, $today, $template ) = @_;
1383 # my $invnum = $self->invnum;
1384 my $cust_main = $self->cust_main;
1385 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1386 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1388 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1389 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1390 #my $balance_due = $self->owed + $pr_total - $cr_total;
1391 my $balance_due = $self->owed + $pr_total;
1394 #my($description,$amount);
1398 foreach ( @pr_cust_bill ) {
1400 "Previous Balance, Invoice #". $_->invnum.
1401 " (". time2str("%x",$_->_date). ")",
1402 $money_char. sprintf("%10.2f",$_->owed)
1405 if (@pr_cust_bill) {
1406 push @buf,['','-----------'];
1407 push @buf,[ 'Total Previous Balance',
1408 $money_char. sprintf("%10.2f",$pr_total ) ];
1413 foreach my $cust_bill_pkg (
1414 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1415 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1418 my $desc = $cust_bill_pkg->desc;
1420 if ( $cust_bill_pkg->pkgnum > 0 ) {
1422 if ( $cust_bill_pkg->setup != 0 ) {
1423 my $description = $desc;
1424 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1425 push @buf, [ $description,
1426 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1428 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1429 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1432 if ( $cust_bill_pkg->recur != 0 ) {
1434 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1435 time2str("%x", $cust_bill_pkg->edate) . ")",
1436 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1439 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1440 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1441 $cust_bill_pkg->sdate );
1444 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1446 } else { #pkgnum tax or one-shot line item
1448 if ( $cust_bill_pkg->setup != 0 ) {
1450 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1452 if ( $cust_bill_pkg->recur != 0 ) {
1453 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1454 . time2str("%x", $cust_bill_pkg->edate). ")",
1455 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1463 push @buf,['','-----------'];
1464 push @buf,['Total New Charges',
1465 $money_char. sprintf("%10.2f",$self->charged) ];
1468 push @buf,['','-----------'];
1469 push @buf,['Total Charges',
1470 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1474 foreach ( $self->cust_credited ) {
1476 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1478 my $reason = substr($_->cust_credit->reason,0,32);
1479 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1480 $reason = " ($reason) " if $reason;
1482 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1484 $money_char. sprintf("%10.2f",$_->amount)
1487 #foreach ( @cr_cust_credit ) {
1489 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1490 # $money_char. sprintf("%10.2f",$_->credited)
1494 #get & print payments
1495 foreach ( $self->cust_bill_pay ) {
1497 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1500 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1501 $money_char. sprintf("%10.2f",$_->amount )
1506 my $balance_due_msg = $self->balance_due_msg;
1508 push @buf,['','-----------'];
1509 push @buf,[$balance_due_msg, $money_char.
1510 sprintf("%10.2f", $balance_due ) ];
1512 #create the template
1513 $template ||= $self->_agent_template;
1514 my $templatefile = 'invoice_template';
1515 $templatefile .= "_$template" if length($template);
1516 my @invoice_template = $conf->config($templatefile)
1517 or die "cannot load config file $templatefile";
1520 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1521 /invoice_lines\((\d*)\)/;
1522 $invoice_lines += $1 || scalar(@buf);
1525 die "no invoice_lines() functions in template?" unless $wasfunc;
1526 my $invoice_template = new Text::Template (
1528 SOURCE => [ map "$_\n", @invoice_template ],
1529 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1530 $invoice_template->compile()
1531 or die "can't compile template: $Text::Template::ERROR";
1533 #setup template variables
1534 package FS::cust_bill::_template; #!
1535 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1537 $invnum = $self->invnum;
1538 $date = $self->_date;
1540 $agent = $self->cust_main->agent->agent;
1542 if ( $FS::cust_bill::invoice_lines ) {
1544 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1546 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1551 #format address (variable for the template)
1553 @address = ( '', '', '', '', '', '' );
1554 package FS::cust_bill; #!
1555 $FS::cust_bill::_template::address[$l++] =
1556 $cust_main->payname.
1557 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1558 ? " (P.O. #". $cust_main->payinfo. ")"
1562 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1563 if $cust_main->company;
1564 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1565 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1566 if $cust_main->address2;
1567 $FS::cust_bill::_template::address[$l++] =
1568 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1570 my $countrydefault = $conf->config('countrydefault') || 'US';
1571 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1572 unless $cust_main->country eq $countrydefault;
1574 # #overdue? (variable for the template)
1575 # $FS::cust_bill::_template::overdue = (
1577 # && $today > $self->_date
1578 ## && $self->printed > 1
1579 # && $self->printed > 0
1582 #and subroutine for the template
1583 sub FS::cust_bill::_template::invoice_lines {
1584 my $lines = shift || scalar(@buf);
1586 scalar(@buf) ? shift @buf : [ '', '' ];
1592 $FS::cust_bill::_template::page = 1;
1596 push @collect, split("\n",
1597 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1599 $FS::cust_bill::_template::page++;
1602 map "$_\n", @collect;
1606 =item print_latex [ TIME [ , TEMPLATE ] ]
1608 Internal method - returns a filename of a filled-in LaTeX template for this
1609 invoice (Note: add ".tex" to get the actual filename).
1611 See print_ps and print_pdf for methods that return PostScript and PDF output.
1613 TIME an optional value used to control the printing of overdue messages. The
1614 default is now. It isn't the date of the invoice; that's the `_date' field.
1615 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1616 L<Time::Local> and L<Date::Parse> for conversion functions.
1620 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1623 my( $self, $today, $template ) = @_;
1625 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1628 my $cust_main = $self->cust_main;
1629 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1630 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1632 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1633 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1634 #my $balance_due = $self->owed + $pr_total - $cr_total;
1635 my $balance_due = $self->owed + $pr_total;
1637 #create the template
1638 $template ||= $self->_agent_template;
1639 my $templatefile = 'invoice_latex';
1640 my $suffix = length($template) ? "_$template" : '';
1641 $templatefile .= $suffix;
1642 my @invoice_template = map "$_\n", $conf->config($templatefile)
1643 or die "cannot load config file $templatefile";
1645 my($format, $text_template);
1646 if ( grep { /^%%Detail/ } @invoice_template ) {
1647 #change this to a die when the old code is removed
1648 warn "old-style invoice template $templatefile; ".
1649 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1652 $format = 'Text::Template';
1653 $text_template = new Text::Template(
1655 SOURCE => \@invoice_template,
1656 DELIMITERS => [ '[@--', '--@]' ],
1659 $text_template->compile()
1660 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1664 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1665 $returnaddress = join("\n",
1666 $conf->config_orbase('invoice_latexreturnaddress', $template)
1669 $returnaddress = '~';
1672 my %invoice_data = (
1673 'invnum' => $self->invnum,
1674 'date' => time2str('%b %o, %Y', $self->_date),
1675 'today' => time2str('%b %o, %Y', $today),
1676 'agent' => _latex_escape($cust_main->agent->agent),
1677 'payname' => _latex_escape($cust_main->payname),
1678 'company' => _latex_escape($cust_main->company),
1679 'address1' => _latex_escape($cust_main->address1),
1680 'address2' => _latex_escape($cust_main->address2),
1681 'city' => _latex_escape($cust_main->city),
1682 'state' => _latex_escape($cust_main->state),
1683 'zip' => _latex_escape($cust_main->zip),
1684 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1685 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1686 'returnaddress' => $returnaddress,
1688 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1689 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1690 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1693 my $countrydefault = $conf->config('countrydefault') || 'US';
1694 if ( $cust_main->country eq $countrydefault ) {
1695 $invoice_data{'country'} = '';
1697 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1700 $invoice_data{'notes'} =
1702 # #do variable substitutions in notes
1703 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1704 $conf->config_orbase('invoice_latexnotes', $template)
1706 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1709 $invoice_data{'footer'} =~ s/\n+$//;
1710 $invoice_data{'smallfooter'} =~ s/\n+$//;
1711 $invoice_data{'notes'} =~ s/\n+$//;
1713 $invoice_data{'po_line'} =
1714 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1715 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1719 if ( $format eq 'old' ) {
1722 my @total_item = ();
1723 while ( @invoice_template ) {
1724 my $line = shift @invoice_template;
1726 if ( $line =~ /^%%Detail\s*$/ ) {
1728 while ( ( my $line_item_line = shift @invoice_template )
1729 !~ /^%%EndDetail\s*$/ ) {
1730 push @line_item, $line_item_line;
1732 foreach my $line_item ( $self->_items ) {
1733 #foreach my $line_item ( $self->_items_pkg ) {
1734 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1735 $invoice_data{'description'} =
1736 _latex_escape($line_item->{'description'});
1737 if ( exists $line_item->{'ext_description'} ) {
1738 $invoice_data{'description'} .=
1739 "\\tabularnewline\n~~".
1740 join( "\\tabularnewline\n~~",
1741 map _latex_escape($_), @{$line_item->{'ext_description'}}
1744 $invoice_data{'amount'} = $line_item->{'amount'};
1745 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1747 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1750 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1752 while ( ( my $total_item_line = shift @invoice_template )
1753 !~ /^%%EndTotalDetails\s*$/ ) {
1754 push @total_item, $total_item_line;
1757 my @total_fill = ();
1760 foreach my $tax ( $self->_items_tax ) {
1761 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1762 $taxtotal += $tax->{'amount'};
1763 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1765 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1770 $invoice_data{'total_item'} = 'Sub-total';
1771 $invoice_data{'total_amount'} =
1772 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1773 unshift @total_fill,
1774 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1778 $invoice_data{'total_item'} = '\textbf{Total}';
1779 $invoice_data{'total_amount'} =
1780 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1782 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1785 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1788 foreach my $credit ( $self->_items_credits ) {
1789 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1791 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1793 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1798 foreach my $payment ( $self->_items_payments ) {
1799 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1801 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1803 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1807 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1808 $invoice_data{'total_amount'} =
1809 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1811 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1814 push @filled_in, @total_fill;
1817 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1818 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1819 push @filled_in, $line;
1830 } elsif ( $format eq 'Text::Template' ) {
1832 my @detail_items = ();
1833 my @total_items = ();
1835 $invoice_data{'detail_items'} = \@detail_items;
1836 $invoice_data{'total_items'} = \@total_items;
1838 foreach my $line_item ( $self->_items ) {
1840 ext_description => [],
1842 $detail->{'ref'} = $line_item->{'pkgnum'};
1843 $detail->{'quantity'} = 1;
1844 $detail->{'description'} = _latex_escape($line_item->{'description'});
1845 if ( exists $line_item->{'ext_description'} ) {
1846 @{$detail->{'ext_description'}} = map {
1848 } @{$line_item->{'ext_description'}};
1850 $detail->{'amount'} = $line_item->{'amount'};
1851 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1853 push @detail_items, $detail;
1858 foreach my $tax ( $self->_items_tax ) {
1860 $total->{'total_item'} = _latex_escape($tax->{'description'});
1861 $taxtotal += $tax->{'amount'};
1862 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1863 push @total_items, $total;
1868 $total->{'total_item'} = 'Sub-total';
1869 $total->{'total_amount'} =
1870 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1871 unshift @total_items, $total;
1876 $total->{'total_item'} = '\textbf{Total}';
1877 $total->{'total_amount'} =
1878 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1879 push @total_items, $total;
1882 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1885 foreach my $credit ( $self->_items_credits ) {
1887 $total->{'total_item'} = _latex_escape($credit->{'description'});
1889 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1890 push @total_items, $total;
1894 foreach my $payment ( $self->_items_payments ) {
1896 $total->{'total_item'} = _latex_escape($payment->{'description'});
1898 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1899 push @total_items, $total;
1904 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1905 $total->{'total_amount'} =
1906 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1907 push @total_items, $total;
1911 die "guru meditation #54";
1914 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1915 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1919 ) or die "can't open temp file: $!\n";
1920 if ( $format eq 'old' ) {
1921 print $fh join('', @filled_in );
1922 } elsif ( $format eq 'Text::Template' ) {
1923 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1925 die "guru meditation #32";
1929 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1934 =item print_ps [ TIME [ , TEMPLATE ] ]
1936 Returns an postscript invoice, as a scalar.
1938 TIME an optional value used to control the printing of overdue messages. The
1939 default is now. It isn't the date of the invoice; that's the `_date' field.
1940 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1941 L<Time::Local> and L<Date::Parse> for conversion functions.
1948 my $file = $self->print_latex(@_);
1950 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1953 my $sfile = shell_quote $file;
1955 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1956 or die "pslatex $file.tex failed; see $file.log for details?\n";
1957 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1958 or die "pslatex $file.tex failed; see $file.log for details?\n";
1960 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1961 or die "dvips failed";
1963 open(POSTSCRIPT, "<$file.ps")
1964 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1966 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1969 while (<POSTSCRIPT>) {
1979 =item print_pdf [ TIME [ , TEMPLATE ] ]
1981 Returns an PDF invoice, as a scalar.
1983 TIME an optional value used to control the printing of overdue messages. The
1984 default is now. It isn't the date of the invoice; that's the `_date' field.
1985 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1986 L<Time::Local> and L<Date::Parse> for conversion functions.
1993 my $file = $self->print_latex(@_);
1995 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1998 #system('pdflatex', "$file.tex");
1999 #system('pdflatex', "$file.tex");
2000 #! LaTeX Error: Unknown graphics extension: .eps.
2002 my $sfile = shell_quote $file;
2004 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2005 or die "pslatex $file.tex failed; see $file.log for details?\n";
2006 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2007 or die "pslatex $file.tex failed; see $file.log for details?\n";
2009 #system('dvipdf', "$file.dvi", "$file.pdf" );
2011 "dvips -q -t letter -f $sfile.dvi ".
2012 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2015 or die "dvips | gs failed: $!";
2017 open(PDF, "<$file.pdf")
2018 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2020 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2033 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2035 Returns an HTML invoice, as a scalar.
2037 TIME an optional value used to control the printing of overdue messages. The
2038 default is now. It isn't the date of the invoice; that's the `_date' field.
2039 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2040 L<Time::Local> and L<Date::Parse> for conversion functions.
2042 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2043 when emailing the invoice as part of a multipart/related MIME email.
2047 #some falze laziness w/print_text and print_latex (and send_csv)
2049 my( $self, $today, $template, $cid ) = @_;
2052 my $cust_main = $self->cust_main;
2053 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2054 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2056 $template ||= $self->_agent_template;
2057 my $templatefile = 'invoice_html';
2058 my $suffix = length($template) ? "_$template" : '';
2059 $templatefile .= $suffix;
2060 my @html_template = map "$_\n", $conf->config($templatefile)
2061 or die "cannot load config file $templatefile";
2063 my $html_template = new Text::Template(
2065 SOURCE => \@html_template,
2066 DELIMITERS => [ '<%=', '%>' ],
2069 $html_template->compile()
2070 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2072 my %invoice_data = (
2073 'invnum' => $self->invnum,
2074 'date' => time2str('%b %o, %Y', $self->_date),
2075 'today' => time2str('%b %o, %Y', $today),
2076 'agent' => encode_entities($cust_main->agent->agent),
2077 'payname' => encode_entities($cust_main->payname),
2078 'company' => encode_entities($cust_main->company),
2079 'address1' => encode_entities($cust_main->address1),
2080 'address2' => encode_entities($cust_main->address2),
2081 'city' => encode_entities($cust_main->city),
2082 'state' => encode_entities($cust_main->state),
2083 'zip' => encode_entities($cust_main->zip),
2084 'terms' => $conf->config('invoice_default_terms')
2085 || 'Payable upon receipt',
2087 'template' => $template,
2088 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2092 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2093 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2095 $invoice_data{'returnaddress'} =
2096 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2098 $invoice_data{'returnaddress'} =
2101 s/\\\\\*?\s*$/<BR>/;
2102 s/\\hyphenation\{[\w\s\-]+\}//;
2105 $conf->config_orbase( 'invoice_latexreturnaddress',
2111 my $countrydefault = $conf->config('countrydefault') || 'US';
2112 if ( $cust_main->country eq $countrydefault ) {
2113 $invoice_data{'country'} = '';
2115 $invoice_data{'country'} =
2116 encode_entities(code2country($cust_main->country));
2120 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2121 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2123 $invoice_data{'notes'} =
2124 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2126 $invoice_data{'notes'} =
2128 s/%%(.*)$/<!-- $1 -->/;
2129 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2130 s/\\begin\{enumerate\}/<ol>/;
2132 s/\\end\{enumerate\}/<\/ol>/;
2133 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2136 $conf->config_orbase('invoice_latexnotes', $template)
2140 # #do variable substitutions in notes
2141 # $invoice_data{'notes'} =
2143 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2144 # $conf->config_orbase('invoice_latexnotes', $suffix)
2148 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2149 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2151 $invoice_data{'footer'} =
2152 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2154 $invoice_data{'footer'} =
2155 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2156 $conf->config_orbase('invoice_latexfooter', $template)
2160 $invoice_data{'po_line'} =
2161 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2162 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2165 my $money_char = $conf->config('money_char') || '$';
2167 foreach my $line_item ( $self->_items ) {
2169 ext_description => [],
2171 $detail->{'ref'} = $line_item->{'pkgnum'};
2172 $detail->{'description'} = encode_entities($line_item->{'description'});
2173 if ( exists $line_item->{'ext_description'} ) {
2174 @{$detail->{'ext_description'}} = map {
2175 encode_entities($_);
2176 } @{$line_item->{'ext_description'}};
2178 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2179 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2181 push @{$invoice_data{'detail_items'}}, $detail;
2186 foreach my $tax ( $self->_items_tax ) {
2188 $total->{'total_item'} = encode_entities($tax->{'description'});
2189 $taxtotal += $tax->{'amount'};
2190 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2191 push @{$invoice_data{'total_items'}}, $total;
2196 $total->{'total_item'} = 'Sub-total';
2197 $total->{'total_amount'} =
2198 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2199 unshift @{$invoice_data{'total_items'}}, $total;
2202 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2205 $total->{'total_item'} = '<b>Total</b>';
2206 $total->{'total_amount'} =
2207 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2208 push @{$invoice_data{'total_items'}}, $total;
2211 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2214 foreach my $credit ( $self->_items_credits ) {
2216 $total->{'total_item'} = encode_entities($credit->{'description'});
2218 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2219 push @{$invoice_data{'total_items'}}, $total;
2223 foreach my $payment ( $self->_items_payments ) {
2225 $total->{'total_item'} = encode_entities($payment->{'description'});
2227 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2228 push @{$invoice_data{'total_items'}}, $total;
2233 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2234 $total->{'total_amount'} =
2235 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2236 push @{$invoice_data{'total_items'}}, $total;
2239 $html_template->fill_in( HASH => \%invoice_data);
2242 # quick subroutine for print_latex
2244 # There are ten characters that LaTeX treats as special characters, which
2245 # means that they do not simply typeset themselves:
2246 # # $ % & ~ _ ^ \ { }
2248 # TeX ignores blanks following an escaped character; if you want a blank (as
2249 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2253 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2254 $value =~ s/([<>])/\$$1\$/g;
2258 #utility methods for print_*
2260 sub balance_due_msg {
2262 my $msg = 'Balance Due';
2263 return $msg unless $conf->exists('invoice_default_terms');
2264 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2265 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2266 } elsif ( $conf->config('invoice_default_terms') ) {
2267 $msg .= ' - '. $conf->config('invoice_default_terms');
2274 my @display = scalar(@_)
2276 : qw( _items_previous _items_pkg );
2277 #: qw( _items_pkg );
2278 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2280 foreach my $display ( @display ) {
2281 push @b, $self->$display(@_);
2286 sub _items_previous {
2288 my $cust_main = $self->cust_main;
2289 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2291 foreach ( @pr_cust_bill ) {
2293 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2294 ' ('. time2str('%x',$_->_date). ')',
2295 #'pkgpart' => 'N/A',
2297 'amount' => sprintf("%.2f", $_->owed),
2303 # 'description' => 'Previous Balance',
2304 # #'pkgpart' => 'N/A',
2305 # 'pkgnum' => 'N/A',
2306 # 'amount' => sprintf("%10.2f", $pr_total ),
2307 # 'ext_description' => [ map {
2308 # "Invoice ". $_->invnum.
2309 # " (". time2str("%x",$_->_date). ") ".
2310 # sprintf("%10.2f", $_->owed)
2311 # } @pr_cust_bill ],
2318 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2319 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2324 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2325 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2328 sub _items_cust_bill_pkg {
2330 my $cust_bill_pkg = shift;
2333 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2335 my $desc = $cust_bill_pkg->desc;
2337 if ( $cust_bill_pkg->pkgnum > 0 ) {
2339 if ( $cust_bill_pkg->setup != 0 ) {
2340 my $description = $desc;
2341 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2342 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2343 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2345 description => $description,
2346 #pkgpart => $part_pkg->pkgpart,
2347 pkgnum => $cust_bill_pkg->pkgnum,
2348 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2349 ext_description => \@d,
2353 if ( $cust_bill_pkg->recur != 0 ) {
2355 description => "$desc (" .
2356 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2357 time2str('%x', $cust_bill_pkg->edate). ')',
2358 #pkgpart => $part_pkg->pkgpart,
2359 pkgnum => $cust_bill_pkg->pkgnum,
2360 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2362 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2363 $cust_bill_pkg->sdate),
2364 $cust_bill_pkg->details,
2369 } else { #pkgnum tax or one-shot line item (??)
2371 if ( $cust_bill_pkg->setup != 0 ) {
2373 'description' => $desc,
2374 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2377 if ( $cust_bill_pkg->recur != 0 ) {
2379 'description' => "$desc (".
2380 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2381 time2str("%x", $cust_bill_pkg->edate). ')',
2382 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2394 sub _items_credits {
2399 foreach ( $self->cust_credited ) {
2401 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2403 my $reason = $_->cust_credit->reason;
2404 #my $reason = substr($_->cust_credit->reason,0,32);
2405 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2406 $reason = " ($reason) " if $reason;
2408 #'description' => 'Credit ref\#'. $_->crednum.
2409 # " (". time2str("%x",$_->cust_credit->_date) .")".
2411 'description' => 'Credit applied '.
2412 time2str("%x",$_->cust_credit->_date). $reason,
2413 'amount' => sprintf("%.2f",$_->amount),
2416 #foreach ( @cr_cust_credit ) {
2418 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2419 # $money_char. sprintf("%10.2f",$_->credited)
2427 sub _items_payments {
2431 #get & print payments
2432 foreach ( $self->cust_bill_pay ) {
2434 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2437 'description' => "Payment received ".
2438 time2str("%x",$_->cust_pay->_date ),
2439 'amount' => sprintf("%.2f", $_->amount )
2458 sub process_reprint {
2459 process_re_X('print', @_);
2466 sub process_reemail {
2467 process_re_X('email', @_);
2475 process_re_X('fax', @_);
2478 use Storable qw(thaw);
2482 my( $method, $job ) = ( shift, shift );
2483 warn "process_re_X $method for job $job\n" if $DEBUG;
2485 my $param = thaw(decode_base64(shift));
2486 warn Dumper($param) if $DEBUG;
2497 my($method, $job, %param ) = @_;
2498 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2500 warn "re_X $method for job $job with param:\n".
2501 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2504 #some false laziness w/search/cust_bill.html
2506 my $orderby = 'ORDER BY cust_bill._date';
2510 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2511 push @where, "cust_bill._date >= $1";
2513 if ( $param{'end'} =~ /^(\d+)$/ ) {
2514 push @where, "cust_bill._date < $1";
2516 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2517 push @where, "cust_main.agentnum = $1";
2521 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2522 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2523 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2524 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2526 push @where, "0 != $owed"
2529 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2532 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2534 my $addl_from = 'left join cust_main using ( custnum )';
2536 if ( $param{'newest_percust'} ) {
2537 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2538 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2539 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2542 my @cust_bill = qsearch( 'cust_bill',
2544 "$distinct cust_bill.*",
2550 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2551 foreach my $cust_bill ( @cust_bill ) {
2552 $cust_bill->$method();
2554 if ( $job ) { #progressbar foo
2556 if ( time - $min_sec > $last ) {
2557 my $error = $job->update_statustext(
2558 int( 100 * $num / scalar(@cust_bill) )
2560 die $error if $error;
2575 print_text formatting (and some logic :/) is in source, but needs to be
2576 slurped in from a file. Also number of lines ($=).
2580 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2581 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base