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 $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1288 return '' unless $amount > 0;
1290 my $oldAutoCommit = $FS::UID::AutoCommit;
1291 local $FS::UID::AutoCommit = 0;
1294 my $pay_batch = qsearchs('pay_batch', {'status' => 'O'});
1296 unless ( $pay_batch ) {
1297 $pay_batch = new FS::pay_batch;
1298 $pay_batch->setfield('status' => 'O');
1299 my $error = $pay_batch->insert;
1301 $dbh->rollback if $oldAutoCommit;
1302 die "error creating new batch: $error\n";
1306 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1307 'batchnum' => $pay_batch->getfield('batchnum'),
1308 'custnum' => $cust_main->getfield('custnum'),
1311 my $cust_pay_batch = new FS::cust_pay_batch ( {
1312 'batchnum' => $pay_batch->getfield('batchnum'),
1313 'invnum' => $self->getfield('invnum'), # is there a better value?
1314 'custnum' => $cust_main->getfield('custnum'),
1315 'last' => $cust_main->getfield('last'),
1316 'first' => $cust_main->getfield('first'),
1317 'address1' => $cust_main->getfield('address1'),
1318 'address2' => $cust_main->getfield('address2'),
1319 'city' => $cust_main->getfield('city'),
1320 'state' => $cust_main->getfield('state'),
1321 'zip' => $cust_main->getfield('zip'),
1322 'country' => $cust_main->getfield('country'),
1323 'payby' => $cust_main->payby,
1324 'payinfo' => $cust_main->payinfo,
1325 'exp' => $cust_main->getfield('paydate'),
1326 'payname' => $cust_main->getfield('payname'),
1327 'amount' => $amount, # consolidating
1330 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1331 if $old_cust_pay_batch;
1334 if ($old_cust_pay_batch) {
1335 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1337 $error = $cust_pay_batch->insert;
1341 $dbh->rollback if $oldAutoCommit;
1345 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1349 sub _agent_template {
1351 $self->_agent_plandata('agent_templatename');
1354 sub _agent_invoice_from {
1356 $self->_agent_plandata('agent_invoice_from');
1359 sub _agent_plandata {
1360 my( $self, $option ) = @_;
1362 my $part_bill_event = qsearchs( 'part_bill_event',
1364 'payby' => $self->cust_main->payby,
1365 'plan' => 'send_agent',
1366 'plandata' => { 'op' => '~',
1367 'value' => "(^|\n)agentnum ".
1369 $self->cust_main->agentnum.
1375 'ORDER BY seconds LIMIT 1'
1378 return '' unless $part_bill_event;
1380 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1383 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1384 " plandata for $option";
1390 =item print_text [ TIME [ , TEMPLATE ] ]
1392 Returns an text invoice, as a list of lines.
1394 TIME an optional value used to control the printing of overdue messages. The
1395 default is now. It isn't the date of the invoice; that's the `_date' field.
1396 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1397 L<Time::Local> and L<Date::Parse> for conversion functions.
1401 #still some false laziness w/_items stuff (and send_csv)
1404 my( $self, $today, $template ) = @_;
1407 # my $invnum = $self->invnum;
1408 my $cust_main = $self->cust_main;
1409 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1410 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1412 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1413 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1414 #my $balance_due = $self->owed + $pr_total - $cr_total;
1415 my $balance_due = $self->owed + $pr_total;
1418 #my($description,$amount);
1422 foreach ( @pr_cust_bill ) {
1424 "Previous Balance, Invoice #". $_->invnum.
1425 " (". time2str("%x",$_->_date). ")",
1426 $money_char. sprintf("%10.2f",$_->owed)
1429 if (@pr_cust_bill) {
1430 push @buf,['','-----------'];
1431 push @buf,[ 'Total Previous Balance',
1432 $money_char. sprintf("%10.2f",$pr_total ) ];
1437 foreach my $cust_bill_pkg (
1438 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1439 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1442 my $desc = $cust_bill_pkg->desc;
1444 if ( $cust_bill_pkg->pkgnum > 0 ) {
1446 if ( $cust_bill_pkg->setup != 0 ) {
1447 my $description = $desc;
1448 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1449 push @buf, [ $description,
1450 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1452 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1453 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1456 if ( $cust_bill_pkg->recur != 0 ) {
1458 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1459 time2str("%x", $cust_bill_pkg->edate) . ")",
1460 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1463 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1464 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1465 $cust_bill_pkg->sdate );
1468 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1470 } else { #pkgnum tax or one-shot line item
1472 if ( $cust_bill_pkg->setup != 0 ) {
1474 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1476 if ( $cust_bill_pkg->recur != 0 ) {
1477 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1478 . time2str("%x", $cust_bill_pkg->edate). ")",
1479 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1487 push @buf,['','-----------'];
1488 push @buf,['Total New Charges',
1489 $money_char. sprintf("%10.2f",$self->charged) ];
1492 push @buf,['','-----------'];
1493 push @buf,['Total Charges',
1494 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1498 foreach ( $self->cust_credited ) {
1500 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1502 my $reason = substr($_->cust_credit->reason,0,32);
1503 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1504 $reason = " ($reason) " if $reason;
1506 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1508 $money_char. sprintf("%10.2f",$_->amount)
1511 #foreach ( @cr_cust_credit ) {
1513 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1514 # $money_char. sprintf("%10.2f",$_->credited)
1518 #get & print payments
1519 foreach ( $self->cust_bill_pay ) {
1521 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1524 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1525 $money_char. sprintf("%10.2f",$_->amount )
1530 my $balance_due_msg = $self->balance_due_msg;
1532 push @buf,['','-----------'];
1533 push @buf,[$balance_due_msg, $money_char.
1534 sprintf("%10.2f", $balance_due ) ];
1536 #create the template
1537 $template ||= $self->_agent_template;
1538 my $templatefile = 'invoice_template';
1539 $templatefile .= "_$template" if length($template);
1540 my @invoice_template = $conf->config($templatefile)
1541 or die "cannot load config file $templatefile";
1544 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1545 /invoice_lines\((\d*)\)/;
1546 $invoice_lines += $1 || scalar(@buf);
1549 die "no invoice_lines() functions in template?" unless $wasfunc;
1550 my $invoice_template = new Text::Template (
1552 SOURCE => [ map "$_\n", @invoice_template ],
1553 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1554 $invoice_template->compile()
1555 or die "can't compile template: $Text::Template::ERROR";
1557 #setup template variables
1558 package FS::cust_bill::_template; #!
1559 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1561 $invnum = $self->invnum;
1562 $date = $self->_date;
1564 $agent = $self->cust_main->agent->agent;
1566 if ( $FS::cust_bill::invoice_lines ) {
1568 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1570 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1575 #format address (variable for the template)
1577 @address = ( '', '', '', '', '', '' );
1578 package FS::cust_bill; #!
1579 $FS::cust_bill::_template::address[$l++] =
1580 $cust_main->payname.
1581 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1582 ? " (P.O. #". $cust_main->payinfo. ")"
1586 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1587 if $cust_main->company;
1588 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1589 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1590 if $cust_main->address2;
1591 $FS::cust_bill::_template::address[$l++] =
1592 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1594 my $countrydefault = $conf->config('countrydefault') || 'US';
1595 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1596 unless $cust_main->country eq $countrydefault;
1598 # #overdue? (variable for the template)
1599 # $FS::cust_bill::_template::overdue = (
1601 # && $today > $self->_date
1602 ## && $self->printed > 1
1603 # && $self->printed > 0
1606 #and subroutine for the template
1607 sub FS::cust_bill::_template::invoice_lines {
1608 my $lines = shift || scalar(@buf);
1610 scalar(@buf) ? shift @buf : [ '', '' ];
1616 $FS::cust_bill::_template::page = 1;
1620 push @collect, split("\n",
1621 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1623 $FS::cust_bill::_template::page++;
1626 map "$_\n", @collect;
1630 =item print_latex [ TIME [ , TEMPLATE ] ]
1632 Internal method - returns a filename of a filled-in LaTeX template for this
1633 invoice (Note: add ".tex" to get the actual filename).
1635 See print_ps and print_pdf for methods that return PostScript and PDF output.
1637 TIME an optional value used to control the printing of overdue messages. The
1638 default is now. It isn't the date of the invoice; that's the `_date' field.
1639 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1640 L<Time::Local> and L<Date::Parse> for conversion functions.
1644 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1647 my( $self, $today, $template ) = @_;
1649 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1652 my $cust_main = $self->cust_main;
1653 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1654 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1656 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1657 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1658 #my $balance_due = $self->owed + $pr_total - $cr_total;
1659 my $balance_due = $self->owed + $pr_total;
1661 #create the template
1662 $template ||= $self->_agent_template;
1663 my $templatefile = 'invoice_latex';
1664 my $suffix = length($template) ? "_$template" : '';
1665 $templatefile .= $suffix;
1666 my @invoice_template = map "$_\n", $conf->config($templatefile)
1667 or die "cannot load config file $templatefile";
1669 my($format, $text_template);
1670 if ( grep { /^%%Detail/ } @invoice_template ) {
1671 #change this to a die when the old code is removed
1672 warn "old-style invoice template $templatefile; ".
1673 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1676 $format = 'Text::Template';
1677 $text_template = new Text::Template(
1679 SOURCE => \@invoice_template,
1680 DELIMITERS => [ '[@--', '--@]' ],
1683 $text_template->compile()
1684 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1688 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1689 $returnaddress = join("\n",
1690 $conf->config_orbase('invoice_latexreturnaddress', $template)
1693 $returnaddress = '~';
1696 my %invoice_data = (
1697 'invnum' => $self->invnum,
1698 'date' => time2str('%b %o, %Y', $self->_date),
1699 'today' => time2str('%b %o, %Y', $today),
1700 'agent' => _latex_escape($cust_main->agent->agent),
1701 'payname' => _latex_escape($cust_main->payname),
1702 'company' => _latex_escape($cust_main->company),
1703 'address1' => _latex_escape($cust_main->address1),
1704 'address2' => _latex_escape($cust_main->address2),
1705 'city' => _latex_escape($cust_main->city),
1706 'state' => _latex_escape($cust_main->state),
1707 'zip' => _latex_escape($cust_main->zip),
1708 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1709 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1710 'returnaddress' => $returnaddress,
1712 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1713 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1714 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1717 my $countrydefault = $conf->config('countrydefault') || 'US';
1718 if ( $cust_main->country eq $countrydefault ) {
1719 $invoice_data{'country'} = '';
1721 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1724 $invoice_data{'notes'} =
1726 # #do variable substitutions in notes
1727 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1728 $conf->config_orbase('invoice_latexnotes', $template)
1730 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1733 $invoice_data{'footer'} =~ s/\n+$//;
1734 $invoice_data{'smallfooter'} =~ s/\n+$//;
1735 $invoice_data{'notes'} =~ s/\n+$//;
1737 $invoice_data{'po_line'} =
1738 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1739 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1743 if ( $format eq 'old' ) {
1746 my @total_item = ();
1747 while ( @invoice_template ) {
1748 my $line = shift @invoice_template;
1750 if ( $line =~ /^%%Detail\s*$/ ) {
1752 while ( ( my $line_item_line = shift @invoice_template )
1753 !~ /^%%EndDetail\s*$/ ) {
1754 push @line_item, $line_item_line;
1756 foreach my $line_item ( $self->_items ) {
1757 #foreach my $line_item ( $self->_items_pkg ) {
1758 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1759 $invoice_data{'description'} =
1760 _latex_escape($line_item->{'description'});
1761 if ( exists $line_item->{'ext_description'} ) {
1762 $invoice_data{'description'} .=
1763 "\\tabularnewline\n~~".
1764 join( "\\tabularnewline\n~~",
1765 map _latex_escape($_), @{$line_item->{'ext_description'}}
1768 $invoice_data{'amount'} = $line_item->{'amount'};
1769 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1771 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1774 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1776 while ( ( my $total_item_line = shift @invoice_template )
1777 !~ /^%%EndTotalDetails\s*$/ ) {
1778 push @total_item, $total_item_line;
1781 my @total_fill = ();
1784 foreach my $tax ( $self->_items_tax ) {
1785 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1786 $taxtotal += $tax->{'amount'};
1787 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1789 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1794 $invoice_data{'total_item'} = 'Sub-total';
1795 $invoice_data{'total_amount'} =
1796 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1797 unshift @total_fill,
1798 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1802 $invoice_data{'total_item'} = '\textbf{Total}';
1803 $invoice_data{'total_amount'} =
1804 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1806 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1809 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1812 foreach my $credit ( $self->_items_credits ) {
1813 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1815 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1817 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1822 foreach my $payment ( $self->_items_payments ) {
1823 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1825 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1827 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1831 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1832 $invoice_data{'total_amount'} =
1833 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1835 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1838 push @filled_in, @total_fill;
1841 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1842 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1843 push @filled_in, $line;
1854 } elsif ( $format eq 'Text::Template' ) {
1856 my @detail_items = ();
1857 my @total_items = ();
1859 $invoice_data{'detail_items'} = \@detail_items;
1860 $invoice_data{'total_items'} = \@total_items;
1862 foreach my $line_item ( $self->_items ) {
1864 ext_description => [],
1866 $detail->{'ref'} = $line_item->{'pkgnum'};
1867 $detail->{'quantity'} = 1;
1868 $detail->{'description'} = _latex_escape($line_item->{'description'});
1869 if ( exists $line_item->{'ext_description'} ) {
1870 @{$detail->{'ext_description'}} = map {
1872 } @{$line_item->{'ext_description'}};
1874 $detail->{'amount'} = $line_item->{'amount'};
1875 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1877 push @detail_items, $detail;
1882 foreach my $tax ( $self->_items_tax ) {
1884 $total->{'total_item'} = _latex_escape($tax->{'description'});
1885 $taxtotal += $tax->{'amount'};
1886 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1887 push @total_items, $total;
1892 $total->{'total_item'} = 'Sub-total';
1893 $total->{'total_amount'} =
1894 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1895 unshift @total_items, $total;
1900 $total->{'total_item'} = '\textbf{Total}';
1901 $total->{'total_amount'} =
1902 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1903 push @total_items, $total;
1906 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1909 foreach my $credit ( $self->_items_credits ) {
1911 $total->{'total_item'} = _latex_escape($credit->{'description'});
1913 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1914 push @total_items, $total;
1918 foreach my $payment ( $self->_items_payments ) {
1920 $total->{'total_item'} = _latex_escape($payment->{'description'});
1922 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1923 push @total_items, $total;
1928 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1929 $total->{'total_amount'} =
1930 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1931 push @total_items, $total;
1935 die "guru meditation #54";
1938 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1939 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1943 ) or die "can't open temp file: $!\n";
1944 if ( $format eq 'old' ) {
1945 print $fh join('', @filled_in );
1946 } elsif ( $format eq 'Text::Template' ) {
1947 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1949 die "guru meditation #32";
1953 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1958 =item print_ps [ TIME [ , TEMPLATE ] ]
1960 Returns an postscript invoice, as a scalar.
1962 TIME an optional value used to control the printing of overdue messages. The
1963 default is now. It isn't the date of the invoice; that's the `_date' field.
1964 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1965 L<Time::Local> and L<Date::Parse> for conversion functions.
1972 my $file = $self->print_latex(@_);
1974 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1977 my $sfile = shell_quote $file;
1979 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1980 or die "pslatex $file.tex failed; see $file.log for details?\n";
1981 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1982 or die "pslatex $file.tex failed; see $file.log for details?\n";
1984 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1985 or die "dvips failed";
1987 open(POSTSCRIPT, "<$file.ps")
1988 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1990 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1993 while (<POSTSCRIPT>) {
2003 =item print_pdf [ TIME [ , TEMPLATE ] ]
2005 Returns an PDF invoice, as a scalar.
2007 TIME an optional value used to control the printing of overdue messages. The
2008 default is now. It isn't the date of the invoice; that's the `_date' field.
2009 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2010 L<Time::Local> and L<Date::Parse> for conversion functions.
2017 my $file = $self->print_latex(@_);
2019 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2022 #system('pdflatex', "$file.tex");
2023 #system('pdflatex', "$file.tex");
2024 #! LaTeX Error: Unknown graphics extension: .eps.
2026 my $sfile = shell_quote $file;
2028 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2029 or die "pslatex $file.tex failed; see $file.log for details?\n";
2030 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2031 or die "pslatex $file.tex failed; see $file.log for details?\n";
2033 #system('dvipdf', "$file.dvi", "$file.pdf" );
2035 "dvips -q -t letter -f $sfile.dvi ".
2036 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2039 or die "dvips | gs failed: $!";
2041 open(PDF, "<$file.pdf")
2042 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2044 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2057 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2059 Returns an HTML invoice, as a scalar.
2061 TIME an optional value used to control the printing of overdue messages. The
2062 default is now. It isn't the date of the invoice; that's the `_date' field.
2063 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2064 L<Time::Local> and L<Date::Parse> for conversion functions.
2066 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2067 when emailing the invoice as part of a multipart/related MIME email.
2071 #some falze laziness w/print_text and print_latex (and send_csv)
2073 my( $self, $today, $template, $cid ) = @_;
2076 my $cust_main = $self->cust_main;
2077 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2078 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2080 $template ||= $self->_agent_template;
2081 my $templatefile = 'invoice_html';
2082 my $suffix = length($template) ? "_$template" : '';
2083 $templatefile .= $suffix;
2084 my @html_template = map "$_\n", $conf->config($templatefile)
2085 or die "cannot load config file $templatefile";
2087 my $html_template = new Text::Template(
2089 SOURCE => \@html_template,
2090 DELIMITERS => [ '<%=', '%>' ],
2093 $html_template->compile()
2094 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2096 my %invoice_data = (
2097 'invnum' => $self->invnum,
2098 'date' => time2str('%b %o, %Y', $self->_date),
2099 'today' => time2str('%b %o, %Y', $today),
2100 'agent' => encode_entities($cust_main->agent->agent),
2101 'payname' => encode_entities($cust_main->payname),
2102 'company' => encode_entities($cust_main->company),
2103 'address1' => encode_entities($cust_main->address1),
2104 'address2' => encode_entities($cust_main->address2),
2105 'city' => encode_entities($cust_main->city),
2106 'state' => encode_entities($cust_main->state),
2107 'zip' => encode_entities($cust_main->zip),
2108 'terms' => $conf->config('invoice_default_terms')
2109 || 'Payable upon receipt',
2111 'template' => $template,
2112 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2116 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2117 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2119 $invoice_data{'returnaddress'} =
2120 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2122 $invoice_data{'returnaddress'} =
2125 s/\\\\\*?\s*$/<BR>/;
2126 s/\\hyphenation\{[\w\s\-]+\}//;
2129 $conf->config_orbase( 'invoice_latexreturnaddress',
2135 my $countrydefault = $conf->config('countrydefault') || 'US';
2136 if ( $cust_main->country eq $countrydefault ) {
2137 $invoice_data{'country'} = '';
2139 $invoice_data{'country'} =
2140 encode_entities(code2country($cust_main->country));
2144 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2145 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2147 $invoice_data{'notes'} =
2148 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2150 $invoice_data{'notes'} =
2152 s/%%(.*)$/<!-- $1 -->/;
2153 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2154 s/\\begin\{enumerate\}/<ol>/;
2156 s/\\end\{enumerate\}/<\/ol>/;
2157 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2160 $conf->config_orbase('invoice_latexnotes', $template)
2164 # #do variable substitutions in notes
2165 # $invoice_data{'notes'} =
2167 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2168 # $conf->config_orbase('invoice_latexnotes', $suffix)
2172 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2173 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2175 $invoice_data{'footer'} =
2176 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2178 $invoice_data{'footer'} =
2179 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2180 $conf->config_orbase('invoice_latexfooter', $template)
2184 $invoice_data{'po_line'} =
2185 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2186 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2189 my $money_char = $conf->config('money_char') || '$';
2191 foreach my $line_item ( $self->_items ) {
2193 ext_description => [],
2195 $detail->{'ref'} = $line_item->{'pkgnum'};
2196 $detail->{'description'} = encode_entities($line_item->{'description'});
2197 if ( exists $line_item->{'ext_description'} ) {
2198 @{$detail->{'ext_description'}} = map {
2199 encode_entities($_);
2200 } @{$line_item->{'ext_description'}};
2202 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2203 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2205 push @{$invoice_data{'detail_items'}}, $detail;
2210 foreach my $tax ( $self->_items_tax ) {
2212 $total->{'total_item'} = encode_entities($tax->{'description'});
2213 $taxtotal += $tax->{'amount'};
2214 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2215 push @{$invoice_data{'total_items'}}, $total;
2220 $total->{'total_item'} = 'Sub-total';
2221 $total->{'total_amount'} =
2222 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2223 unshift @{$invoice_data{'total_items'}}, $total;
2226 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2229 $total->{'total_item'} = '<b>Total</b>';
2230 $total->{'total_amount'} =
2231 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2232 push @{$invoice_data{'total_items'}}, $total;
2235 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2238 foreach my $credit ( $self->_items_credits ) {
2240 $total->{'total_item'} = encode_entities($credit->{'description'});
2242 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2243 push @{$invoice_data{'total_items'}}, $total;
2247 foreach my $payment ( $self->_items_payments ) {
2249 $total->{'total_item'} = encode_entities($payment->{'description'});
2251 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2252 push @{$invoice_data{'total_items'}}, $total;
2257 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2258 $total->{'total_amount'} =
2259 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2260 push @{$invoice_data{'total_items'}}, $total;
2263 $html_template->fill_in( HASH => \%invoice_data);
2266 # quick subroutine for print_latex
2268 # There are ten characters that LaTeX treats as special characters, which
2269 # means that they do not simply typeset themselves:
2270 # # $ % & ~ _ ^ \ { }
2272 # TeX ignores blanks following an escaped character; if you want a blank (as
2273 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2277 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2278 $value =~ s/([<>])/\$$1\$/g;
2282 #utility methods for print_*
2284 sub balance_due_msg {
2286 my $msg = 'Balance Due';
2287 return $msg unless $conf->exists('invoice_default_terms');
2288 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2289 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2290 } elsif ( $conf->config('invoice_default_terms') ) {
2291 $msg .= ' - '. $conf->config('invoice_default_terms');
2298 my @display = scalar(@_)
2300 : qw( _items_previous _items_pkg );
2301 #: qw( _items_pkg );
2302 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2304 foreach my $display ( @display ) {
2305 push @b, $self->$display(@_);
2310 sub _items_previous {
2312 my $cust_main = $self->cust_main;
2313 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2315 foreach ( @pr_cust_bill ) {
2317 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2318 ' ('. time2str('%x',$_->_date). ')',
2319 #'pkgpart' => 'N/A',
2321 'amount' => sprintf("%.2f", $_->owed),
2327 # 'description' => 'Previous Balance',
2328 # #'pkgpart' => 'N/A',
2329 # 'pkgnum' => 'N/A',
2330 # 'amount' => sprintf("%10.2f", $pr_total ),
2331 # 'ext_description' => [ map {
2332 # "Invoice ". $_->invnum.
2333 # " (". time2str("%x",$_->_date). ") ".
2334 # sprintf("%10.2f", $_->owed)
2335 # } @pr_cust_bill ],
2342 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2343 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2348 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2349 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2352 sub _items_cust_bill_pkg {
2354 my $cust_bill_pkg = shift;
2357 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2359 my $desc = $cust_bill_pkg->desc;
2361 if ( $cust_bill_pkg->pkgnum > 0 ) {
2363 if ( $cust_bill_pkg->setup != 0 ) {
2364 my $description = $desc;
2365 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2366 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2367 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2369 description => $description,
2370 #pkgpart => $part_pkg->pkgpart,
2371 pkgnum => $cust_bill_pkg->pkgnum,
2372 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2373 ext_description => \@d,
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 #pkgpart => $part_pkg->pkgpart,
2383 pkgnum => $cust_bill_pkg->pkgnum,
2384 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2386 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2387 $cust_bill_pkg->sdate),
2388 $cust_bill_pkg->details,
2393 } else { #pkgnum tax or one-shot line item (??)
2395 if ( $cust_bill_pkg->setup != 0 ) {
2397 'description' => $desc,
2398 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2401 if ( $cust_bill_pkg->recur != 0 ) {
2403 'description' => "$desc (".
2404 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2405 time2str("%x", $cust_bill_pkg->edate). ')',
2406 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2418 sub _items_credits {
2423 foreach ( $self->cust_credited ) {
2425 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2427 my $reason = $_->cust_credit->reason;
2428 #my $reason = substr($_->cust_credit->reason,0,32);
2429 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2430 $reason = " ($reason) " if $reason;
2432 #'description' => 'Credit ref\#'. $_->crednum.
2433 # " (". time2str("%x",$_->cust_credit->_date) .")".
2435 'description' => 'Credit applied '.
2436 time2str("%x",$_->cust_credit->_date). $reason,
2437 'amount' => sprintf("%.2f",$_->amount),
2440 #foreach ( @cr_cust_credit ) {
2442 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2443 # $money_char. sprintf("%10.2f",$_->credited)
2451 sub _items_payments {
2455 #get & print payments
2456 foreach ( $self->cust_bill_pay ) {
2458 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2461 'description' => "Payment received ".
2462 time2str("%x",$_->cust_pay->_date ),
2463 'amount' => sprintf("%.2f", $_->amount )
2482 sub process_reprint {
2483 process_re_X('print', @_);
2490 sub process_reemail {
2491 process_re_X('email', @_);
2499 process_re_X('fax', @_);
2502 use Storable qw(thaw);
2506 my( $method, $job ) = ( shift, shift );
2507 warn "process_re_X $method for job $job\n" if $DEBUG;
2509 my $param = thaw(decode_base64(shift));
2510 warn Dumper($param) if $DEBUG;
2521 my($method, $job, %param ) = @_;
2522 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2524 warn "re_X $method for job $job with param:\n".
2525 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2528 #some false laziness w/search/cust_bill.html
2530 my $orderby = 'ORDER BY cust_bill._date';
2534 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2535 push @where, "cust_bill._date >= $1";
2537 if ( $param{'end'} =~ /^(\d+)$/ ) {
2538 push @where, "cust_bill._date < $1";
2540 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2541 push @where, "cust_main.agentnum = $1";
2545 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2546 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2547 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2548 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2550 push @where, "0 != $owed"
2553 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2556 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2558 my $addl_from = 'left join cust_main using ( custnum )';
2560 if ( $param{'newest_percust'} ) {
2561 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2562 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2563 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2566 my @cust_bill = qsearch( 'cust_bill',
2568 "$distinct cust_bill.*",
2574 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2575 foreach my $cust_bill ( @cust_bill ) {
2576 $cust_bill->$method();
2578 if ( $job ) { #progressbar foo
2580 if ( time - $min_sec > $last ) {
2581 my $error = $job->update_statustext(
2582 int( 100 * $num / scalar(@cust_bill) )
2584 die $error if $error;
2599 print_text formatting (and some logic :/) is in source, but needs to be
2600 slurped in from a file. Also number of lines ($=).
2604 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2605 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base