4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
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 generate_ps do_print );
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::cust_bill_pay_batch;
30 use FS::part_bill_event;
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
36 $me = '[FS::cust_bill]';
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub {
41 $money_char = $conf->config('money_char') || '$';
46 FS::cust_bill - Object methods for cust_bill records
52 $record = new FS::cust_bill \%hash;
53 $record = new FS::cust_bill { 'column' => 'value' };
55 $error = $record->insert;
57 $error = $new_record->replace($old_record);
59 $error = $record->delete;
61 $error = $record->check;
63 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
65 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
67 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
69 @cust_pay_objects = $cust_bill->cust_pay;
71 $tax_amount = $record->tax;
73 @lines = $cust_bill->print_text;
74 @lines = $cust_bill->print_text $time;
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
81 following fields are currently supported:
85 =item invnum - primary key (assigned automatically for new invoices)
87 =item custnum - customer (see L<FS::cust_main>)
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
92 =item charged - amount of this invoice
94 =item printed - deprecated
96 =item closed - books closed flag, empty or `Y'
106 Creates a new invoice. To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
112 sub table { 'cust_bill'; }
114 sub cust_linked { $_[0]->cust_main_custnum; }
115 sub cust_unlinked_msg {
117 "WARNING: can't find cust_main.custnum ". $self->custnum.
118 ' (cust_bill.invnum '. $self->invnum. ')';
123 Adds this invoice to the database ("Posts" the invoice). If there is an error,
124 returns the error, otherwise returns false.
128 This method now works but you probably shouldn't use it. Instead, apply a
129 credit against the invoice.
131 Using this method to delete invoices outright is really, really bad. There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
135 Really, don't use it.
141 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
142 $self->SUPER::delete(@_);
145 =item replace OLD_RECORD
147 Replaces the OLD_RECORD with this one in the database. If there is an error,
148 returns the error, otherwise returns false.
150 Only printed may be changed. printed is normally updated by calling the
151 collect method of a customer object (see L<FS::cust_main>).
155 #replace can be inherited from Record.pm
157 # replace_check is now the preferred way to #implement replace data checks
158 # (so $object->replace() works without an argument)
161 my( $new, $old ) = ( shift, shift );
162 return "Can't change custnum!" unless $old->custnum == $new->custnum;
163 #return "Can't change _date!" unless $old->_date eq $new->_date;
164 return "Can't change _date!" unless $old->_date == $new->_date;
165 return "Can't change charged!" unless $old->charged == $new->charged
166 || $old->charged == 0;
173 Checks all fields to make sure this is a valid invoice. If there is an error,
174 returns the error, otherwise returns false. Called by the insert and replace
183 $self->ut_numbern('invnum')
184 || $self->ut_number('custnum')
185 || $self->ut_numbern('_date')
186 || $self->ut_money('charged')
187 || $self->ut_numbern('printed')
188 || $self->ut_enum('closed', [ '', 'Y' ])
190 return $error if $error;
192 return "Unknown customer"
193 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
195 $self->_date(time) unless $self->_date;
197 $self->printed(0) if $self->printed eq '';
204 Returns a list consisting of the total previous balance for this customer,
205 followed by the previous outstanding invoices (as FS::cust_bill objects also).
212 my @cust_bill = sort { $a->_date <=> $b->_date }
213 grep { $_->owed != 0 && $_->_date < $self->_date }
214 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
216 foreach ( @cust_bill ) { $total += $_->owed; }
222 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
228 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
233 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
240 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
242 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
245 =item open_cust_bill_pkg
247 Returns the open line items for this invoice.
249 Note that cust_bill_pkg with both setup and recur fees are returned as two
250 separate line items, each with only one fee.
254 # modeled after cust_main::open_cust_bill
255 sub open_cust_bill_pkg {
258 # grep { $_->owed > 0 } $self->cust_bill_pkg
260 my %other = ( 'recur' => 'setup',
261 'setup' => 'recur', );
263 foreach my $field ( qw( recur setup )) {
264 push @open, map { $_->set( $other{$field}, 0 ); $_; }
265 grep { $_->owed($field) > 0 }
266 $self->cust_bill_pkg;
272 =item cust_bill_event
274 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
279 sub cust_bill_event {
281 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
287 Returns the customer (see L<FS::cust_main>) for this invoice.
293 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
296 =item cust_suspend_if_balance_over AMOUNT
298 Suspends the customer associated with this invoice if the total amount owed on
299 this invoice and all older invoices is greater than the specified amount.
301 Returns a list: an empty list on success or a list of errors.
305 sub cust_suspend_if_balance_over {
306 my( $self, $amount ) = ( shift, shift );
307 my $cust_main = $self->cust_main;
308 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
311 $cust_main->suspend(@_);
317 Depreciated. See the cust_credited method.
319 #Returns a list consisting of the total previous credited (see
320 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
321 #outstanding credits (FS::cust_credit objects).
327 croak "FS::cust_bill->cust_credit depreciated; see ".
328 "FS::cust_bill->cust_credit_bill";
331 #my @cust_credit = sort { $a->_date <=> $b->_date }
332 # grep { $_->credited != 0 && $_->_date < $self->_date }
333 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
335 #foreach (@cust_credit) { $total += $_->credited; }
336 #$total, @cust_credit;
341 Depreciated. See the cust_bill_pay method.
343 #Returns all payments (see L<FS::cust_pay>) for this invoice.
349 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
351 #sort { $a->_date <=> $b->_date }
352 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
358 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
364 sort { $a->_date <=> $b->_date }
365 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
370 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
376 sort { $a->_date <=> $b->_date }
377 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
383 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
390 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
392 foreach (@taxlines) { $total += $_->setup; }
398 Returns the amount owed (still outstanding) on this invoice, which is charged
399 minus all payment applications (see L<FS::cust_bill_pay>) and credit
400 applications (see L<FS::cust_credit_bill>).
406 my $balance = $self->charged;
407 $balance -= $_->amount foreach ( $self->cust_bill_pay );
408 $balance -= $_->amount foreach ( $self->cust_credited );
409 $balance = sprintf( "%.2f", $balance);
410 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
414 =item apply_payments_and_credits
418 sub apply_payments_and_credits {
421 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
422 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
424 while ( $self->owed > 0 and ( @payments || @credits ) ) {
427 if ( @payments && @credits ) {
429 #decide which goes first by weight of top (unapplied) line item
431 my @open_lineitems = $self->open_cust_bill_pkg;
434 max( map { $_->part_pkg->pay_weight || 0 }
439 my $max_credit_weight =
440 max( map { $_->part_pkg->credit_weight || 0 }
446 #if both are the same... payments first? it has to be something
447 if ( $max_pay_weight >= $max_credit_weight ) {
453 } elsif ( @payments ) {
455 } elsif ( @credits ) {
458 die "guru meditation #12 and 35";
461 if ( $app eq 'pay' ) {
463 my $payment = shift @payments;
465 $app = new FS::cust_bill_pay {
466 'paynum' => $payment->paynum,
467 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
470 } elsif ( $app eq 'credit' ) {
472 my $credit = shift @credits;
474 $app = new FS::cust_credit_bill {
475 'crednum' => $credit->crednum,
476 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
480 die "guru meditation #12 and 35";
483 $app->invnum( $self->invnum );
485 my $error = $app->insert;
486 die $error if $error;
492 =item generate_email PARAMHASH
494 PARAMHASH can contain the following:
498 =item from => sender address, required
500 =item tempate => alternate template name, optional
502 =item print_text => text attachment arrayref, optional
504 =item subject => email subject, optional
508 Returns an argument list to be passed to L<FS::Misc::send_email>.
519 my $me = '[FS::cust_bill::generate_email]';
522 'from' => $args{'from'},
523 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
526 if (ref($args{'to'}) eq 'ARRAY') {
527 $return{'to'} = $args{'to'};
529 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
530 $self->cust_main->invoicing_list
534 if ( $conf->exists('invoice_html') ) {
536 warn "$me creating HTML/text multipart message"
539 $return{'nobody'} = 1;
541 my $alternative = build MIME::Entity
542 'Type' => 'multipart/alternative',
543 'Encoding' => '7bit',
544 'Disposition' => 'inline'
548 if ( $conf->exists('invoice_email_pdf')
549 and scalar($conf->config('invoice_email_pdf_note')) ) {
551 warn "$me using 'invoice_email_pdf_note' in multipart message"
553 $data = [ map { $_ . "\n" }
554 $conf->config('invoice_email_pdf_note')
559 warn "$me not using 'invoice_email_pdf_note' in multipart message"
561 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
562 $data = $args{'print_text'};
564 $data = [ $self->print_text('', $args{'template'}) ];
569 $alternative->attach(
570 'Type' => 'text/plain',
571 #'Encoding' => 'quoted-printable',
572 'Encoding' => '7bit',
574 'Disposition' => 'inline',
577 $args{'from'} =~ /\@([\w\.\-]+)/;
578 my $from = $1 || 'example.com';
579 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
581 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
583 if ( defined($args{'template'}) && length($args{'template'})
584 && -e "$path/logo_". $args{'template'}. ".png"
587 $file = "$path/logo_". $args{'template'}. ".png";
589 $file = "$path/logo.png";
592 my $image = build MIME::Entity
593 'Type' => 'image/png',
594 'Encoding' => 'base64',
596 'Filename' => 'logo.png',
597 'Content-ID' => "<$content_id>",
600 $alternative->attach(
601 'Type' => 'text/html',
602 'Encoding' => 'quoted-printable',
603 'Data' => [ '<html>',
606 ' '. encode_entities($return{'subject'}),
609 ' <body bgcolor="#e8e8e8">',
610 $self->print_html('', $args{'template'}, $content_id),
614 'Disposition' => 'inline',
615 #'Filename' => 'invoice.pdf',
618 if ( $conf->exists('invoice_email_pdf') ) {
623 # multipart/alternative
629 my $related = build MIME::Entity 'Type' => 'multipart/related',
630 'Encoding' => '7bit';
632 #false laziness w/Misc::send_email
633 $related->head->replace('Content-type',
635 '; boundary="'. $related->head->multipart_boundary. '"'.
636 '; type=multipart/alternative'
639 $related->add_part($alternative);
641 $related->add_part($image);
643 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
645 $return{'mimeparts'} = [ $related, $pdf ];
649 #no other attachment:
651 # multipart/alternative
656 $return{'content-type'} = 'multipart/related';
657 $return{'mimeparts'} = [ $alternative, $image ];
658 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
659 #$return{'disposition'} = 'inline';
665 if ( $conf->exists('invoice_email_pdf') ) {
666 warn "$me creating PDF attachment"
669 #mime parts arguments a la MIME::Entity->build().
670 $return{'mimeparts'} = [
671 { $self->mimebuild_pdf('', $args{'template'}) }
675 if ( $conf->exists('invoice_email_pdf')
676 and scalar($conf->config('invoice_email_pdf_note')) ) {
678 warn "$me using 'invoice_email_pdf_note'"
680 $return{'body'} = [ map { $_ . "\n" }
681 $conf->config('invoice_email_pdf_note')
686 warn "$me not using 'invoice_email_pdf_note'"
688 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
689 $return{'body'} = $args{'print_text'};
691 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
704 Returns a list suitable for passing to MIME::Entity->build(), representing
705 this invoice as PDF attachment.
712 'Type' => 'application/pdf',
713 'Encoding' => 'base64',
714 'Data' => [ $self->print_pdf(@_) ],
715 'Disposition' => 'attachment',
716 'Filename' => 'invoice.pdf',
720 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
722 Sends this invoice to the destinations configured for this customer: sends
723 email, prints and/or faxes. See L<FS::cust_main_invoice>.
725 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
727 AGENTNUM, if specified, means that this invoice will only be sent for customers
728 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
729 single agent) or an arrayref of agentnums.
731 INVOICE_FROM, if specified, overrides the default email invoice From: address.
738 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
739 or die "invalid invoice number: " . $opt{invnum};
741 my @args = ( $opt{template}, $opt{agentnum} );
742 push @args, $opt{invoice_from}
743 if exists($opt{invoice_from}) && $opt{invoice_from};
745 my $error = $self->send( @args );
746 die $error if $error;
752 my $template = scalar(@_) ? shift : '';
753 if ( scalar(@_) && $_[0] ) {
754 my $agentnums = ref($_[0]) ? shift : [ shift ];
755 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
761 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
763 my @invoicing_list = $self->cust_main->invoicing_list;
765 $self->email($template, $invoice_from)
766 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
768 $self->print($template)
769 if grep { $_ eq 'POST' } @invoicing_list; #postal
771 $self->fax($template)
772 if grep { $_ eq 'FAX' } @invoicing_list; #fax
778 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
782 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
784 INVOICE_FROM, if specified, overrides the default email invoice From: address.
788 sub queueable_email {
791 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
792 or die "invalid invoice number: " . $opt{invnum};
794 my @args = ( $opt{template} );
795 push @args, $opt{invoice_from}
796 if exists($opt{invoice_from}) && $opt{invoice_from};
798 my $error = $self->email( @args );
799 die $error if $error;
805 my $template = scalar(@_) ? shift : '';
809 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
811 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
812 $self->cust_main->invoicing_list;
814 #better to notify this person than silence
815 @invoicing_list = ($invoice_from) unless @invoicing_list;
817 my $error = send_email(
818 $self->generate_email(
819 'from' => $invoice_from,
820 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
821 'template' => $template,
824 die "can't email invoice: $error\n" if $error;
825 #die "$error\n" if $error;
829 =item lpr_data [ TEMPLATENAME ]
831 Returns the postscript or plaintext for this invoice as an arrayref.
833 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
838 my( $self, $template) = @_;
839 $conf->exists('invoice_latex')
840 ? [ $self->print_ps('', $template) ]
841 : [ $self->print_text('', $template) ];
844 =item print [ TEMPLATENAME ]
848 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
854 my $template = scalar(@_) ? shift : '';
856 do_print $self->lpr_data($template);
859 =item fax [ TEMPLATENAME ]
863 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
869 my $template = scalar(@_) ? shift : '';
871 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
872 unless $conf->exists('invoice_latex');
874 my $dialstring = $self->cust_main->getfield('fax');
877 my $error = send_fax( 'docdata' => $self->lpr_data($template),
878 'dialstring' => $dialstring,
880 die $error if $error;
884 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
886 Like B<send>, but only sends the invoice if it is the newest open invoice for
896 grep { $_->owed > 0 }
897 qsearch('cust_bill', {
898 'custnum' => $self->custnum,
899 #'_date' => { op=>'>', value=>$self->_date },
900 'invnum' => { op=>'>', value=>$self->invnum },
907 =item send_csv OPTION => VALUE, ...
909 Sends invoice as a CSV data-file to a remote host with the specified protocol.
913 protocol - currently only "ftp"
919 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
920 and YYMMDDHHMMSS is a timestamp.
922 See L</print_csv> for a description of the output format.
927 my($self, %opt) = @_;
931 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
932 mkdir $spooldir, 0700 unless -d $spooldir;
934 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
935 my $file = "$spooldir/$tracctnum.csv";
937 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
939 open(CSV, ">$file") or die "can't open $file: $!";
947 if ( $opt{protocol} eq 'ftp' ) {
948 eval "use Net::FTP;";
950 $net = Net::FTP->new($opt{server}) or die @$;
952 die "unknown protocol: $opt{protocol}";
955 $net->login( $opt{username}, $opt{password} )
956 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
958 $net->binary or die "can't set binary mode";
960 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
962 $net->put($file) or die "can't put $file: $!";
972 Spools CSV invoice data.
978 =item format - 'default' or 'billco'
980 =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>).
982 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
984 =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.
991 my($self, %opt) = @_;
993 my $cust_main = $self->cust_main;
995 if ( $opt{'dest'} ) {
996 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
997 $cust_main->invoicing_list;
998 return 'N/A' unless $invoicing_list{$opt{'dest'}}
999 || ! keys %invoicing_list;
1002 if ( $opt{'balanceover'} ) {
1004 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1007 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1008 mkdir $spooldir, 0700 unless -d $spooldir;
1010 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1014 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1015 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1018 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1020 open(CSV, ">>$file") or die "can't open $file: $!";
1021 flock(CSV, LOCK_EX);
1026 if ( lc($opt{'format'}) eq 'billco' ) {
1028 flock(CSV, LOCK_UN);
1033 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1036 open(CSV,">>$file") or die "can't open $file: $!";
1037 flock(CSV, LOCK_EX);
1043 flock(CSV, LOCK_UN);
1050 =item print_csv OPTION => VALUE, ...
1052 Returns CSV data for this invoice.
1056 format - 'default' or 'billco'
1058 Returns a list consisting of two scalars. The first is a single line of CSV
1059 header information for this invoice. The second is one or more lines of CSV
1060 detail information for this invoice.
1062 If I<format> is not specified or "default", the fields of the CSV file are as
1065 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1069 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1071 B<record_type> is C<cust_bill> for the initial header line only. The
1072 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1073 fields are filled in.
1075 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1076 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1079 =item invnum - invoice number
1081 =item custnum - customer number
1083 =item _date - invoice date
1085 =item charged - total invoice amount
1087 =item first - customer first name
1089 =item last - customer first name
1091 =item company - company name
1093 =item address1 - address line 1
1095 =item address2 - address line 1
1105 =item pkg - line item description
1107 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1109 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1111 =item sdate - start date for recurring fee
1113 =item edate - end date for recurring fee
1117 If I<format> is "billco", the fields of the header CSV file are as follows:
1119 +-------------------------------------------------------------------+
1120 | FORMAT HEADER FILE |
1121 |-------------------------------------------------------------------|
1122 | Field | Description | Name | Type | Width |
1123 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1124 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1125 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1126 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1127 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1128 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1129 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1130 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1131 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1132 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1133 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1134 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1135 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1136 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1137 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1138 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1139 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1140 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1141 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1142 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1143 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1144 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1145 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1146 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1147 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1148 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1149 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1150 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1151 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1152 +-------+-------------------------------+------------+------+-------+
1154 If I<format> is "billco", the fields of the detail CSV file are as follows:
1156 FORMAT FOR DETAIL FILE
1158 Field | Description | Name | Type | Width
1159 1 | N/A-Leave Empty | RC | CHAR | 2
1160 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1161 3 | Account Number | TRACCTNUM | CHAR | 15
1162 4 | Invoice Number | TRINVOICE | CHAR | 15
1163 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1164 6 | Transaction Detail | DETAILS | CHAR | 100
1165 7 | Amount | AMT | NUM* | 9
1166 8 | Line Format Control** | LNCTRL | CHAR | 2
1167 9 | Grouping Code | GROUP | CHAR | 2
1168 10 | User Defined | ACCT CODE | CHAR | 15
1173 my($self, %opt) = @_;
1175 eval "use Text::CSV_XS";
1178 my $cust_main = $self->cust_main;
1180 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1182 if ( lc($opt{'format'}) eq 'billco' ) {
1185 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1188 if ( $conf->exists('invoice_default_terms')
1189 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1190 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1193 my( $previous_balance, @unused ) = $self->previous; #previous balance
1195 my $pmt_cr_applied = 0;
1196 $pmt_cr_applied += $_->{'amount'}
1197 foreach ( $self->_items_payments, $self->_items_credits ) ;
1199 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1202 '', # 1 | N/A-Leave Empty CHAR 2
1203 '', # 2 | N/A-Leave Empty CHAR 15
1204 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1205 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1206 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1207 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1208 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1209 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1210 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1211 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1212 '', # 10 | Ancillary Billing Information CHAR 30
1213 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1214 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1217 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1220 $duedate, # 14 | Bill Due Date CHAR 10
1222 $previous_balance, # 15 | Previous Balance NUM* 9
1223 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1224 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1225 $totaldue, # 18 | Total Amt Due NUM* 9
1226 $totaldue, # 19 | Total Amt Due NUM* 9
1227 '', # 20 | 30 Day Aging NUM* 9
1228 '', # 21 | 60 Day Aging NUM* 9
1229 '', # 22 | 90 Day Aging NUM* 9
1230 'N', # 23 | Y/N CHAR 1
1231 '', # 24 | Remittance automation CHAR 100
1232 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1233 $self->custnum, # 26 | Customer Reference Number CHAR 15
1234 '0', # 27 | Federal Tax*** NUM* 9
1235 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1236 '0', # 29 | Other Taxes & Fees*** NUM* 9
1245 time2str("%x", $self->_date),
1246 sprintf("%.2f", $self->charged),
1247 ( map { $cust_main->getfield($_) }
1248 qw( first last company address1 address2 city state zip country ) ),
1250 ) or die "can't create csv";
1253 my $header = $csv->string. "\n";
1256 if ( lc($opt{'format'}) eq 'billco' ) {
1259 foreach my $item ( $self->_items_pkg ) {
1262 '', # 1 | N/A-Leave Empty CHAR 2
1263 '', # 2 | N/A-Leave Empty CHAR 15
1264 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1265 $self->invnum, # 4 | Invoice Number CHAR 15
1266 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1267 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1268 $item->{'amount'}, # 7 | Amount NUM* 9
1269 '', # 8 | Line Format Control** CHAR 2
1270 '', # 9 | Grouping Code CHAR 2
1271 '', # 10 | User Defined CHAR 15
1274 $detail .= $csv->string. "\n";
1280 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1282 my($pkg, $setup, $recur, $sdate, $edate);
1283 if ( $cust_bill_pkg->pkgnum ) {
1285 ($pkg, $setup, $recur, $sdate, $edate) = (
1286 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1287 ( $cust_bill_pkg->setup != 0
1288 ? sprintf("%.2f", $cust_bill_pkg->setup )
1290 ( $cust_bill_pkg->recur != 0
1291 ? sprintf("%.2f", $cust_bill_pkg->recur )
1293 ( $cust_bill_pkg->sdate
1294 ? time2str("%x", $cust_bill_pkg->sdate)
1296 ($cust_bill_pkg->edate
1297 ?time2str("%x", $cust_bill_pkg->edate)
1301 } else { #pkgnum tax
1302 next unless $cust_bill_pkg->setup != 0;
1303 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1304 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1306 ($pkg, $setup, $recur, $sdate, $edate) =
1307 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1313 ( map { '' } (1..11) ),
1314 ($pkg, $setup, $recur, $sdate, $edate)
1315 ) or die "can't create csv";
1317 $detail .= $csv->string. "\n";
1323 ( $header, $detail );
1329 Pays this invoice with a compliemntary payment. If there is an error,
1330 returns the error, otherwise returns false.
1336 my $cust_pay = new FS::cust_pay ( {
1337 'invnum' => $self->invnum,
1338 'paid' => $self->owed,
1341 'payinfo' => $self->cust_main->payinfo,
1349 Attempts to pay this invoice with a credit card payment via a
1350 Business::OnlinePayment realtime gateway. See
1351 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1352 for supported processors.
1358 $self->realtime_bop( 'CC', @_ );
1363 Attempts to pay this invoice with an electronic check (ACH) payment via a
1364 Business::OnlinePayment realtime gateway. See
1365 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1366 for supported processors.
1372 $self->realtime_bop( 'ECHECK', @_ );
1377 Attempts to pay this invoice with phone bill (LEC) payment via a
1378 Business::OnlinePayment realtime gateway. See
1379 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1380 for supported processors.
1386 $self->realtime_bop( 'LEC', @_ );
1390 my( $self, $method ) = @_;
1392 my $cust_main = $self->cust_main;
1393 my $balance = $cust_main->balance;
1394 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1395 $amount = sprintf("%.2f", $amount);
1396 return "not run (balance $balance)" unless $amount > 0;
1398 my $description = 'Internet Services';
1399 if ( $conf->exists('business-onlinepayment-description') ) {
1400 my $dtempl = $conf->config('business-onlinepayment-description');
1402 my $agent_obj = $cust_main->agent
1403 or die "can't retreive agent for $cust_main (agentnum ".
1404 $cust_main->agentnum. ")";
1405 my $agent = $agent_obj->agent;
1406 my $pkgs = join(', ',
1407 map { $_->cust_pkg->part_pkg->pkg }
1408 grep { $_->pkgnum } $self->cust_bill_pkg
1410 $description = eval qq("$dtempl");
1413 $cust_main->realtime_bop($method, $amount,
1414 'description' => $description,
1415 'invnum' => $self->invnum,
1420 =item batch_card OPTION => VALUE...
1422 Adds a payment for this invoice to the pending credit card batch (see
1423 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1424 runs the payment using a realtime gateway.
1429 my ($self, %options) = @_;
1430 my $cust_main = $self->cust_main;
1432 $options{invnum} = $self->invnum;
1434 $cust_main->batch_card(%options);
1437 sub _agent_template {
1439 $self->cust_main->agent_template;
1442 sub _agent_invoice_from {
1444 $self->cust_main->agent_invoice_from;
1447 =item print_text [ TIME [ , TEMPLATE ] ]
1449 Returns an text invoice, as a list of lines.
1451 TIME an optional value used to control the printing of overdue messages. The
1452 default is now. It isn't the date of the invoice; that's the `_date' field.
1453 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1454 L<Time::Local> and L<Date::Parse> for conversion functions.
1458 #still some false laziness w/_items stuff (and send_csv)
1461 my( $self, $today, $template ) = @_;
1464 # my $invnum = $self->invnum;
1465 my $cust_main = $self->cust_main;
1466 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1467 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1469 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1470 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1471 #my $balance_due = $self->owed + $pr_total - $cr_total;
1472 my $balance_due = $self->owed + $pr_total;
1475 #my($description,$amount);
1479 foreach ( @pr_cust_bill ) {
1481 "Previous Balance, Invoice #". $_->invnum.
1482 " (". time2str("%x",$_->_date). ")",
1483 $money_char. sprintf("%10.2f",$_->owed)
1486 if (@pr_cust_bill) {
1487 push @buf,['','-----------'];
1488 push @buf,[ 'Total Previous Balance',
1489 $money_char. sprintf("%10.2f",$pr_total ) ];
1494 foreach my $cust_bill_pkg (
1495 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1496 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1499 my $desc = $cust_bill_pkg->desc;
1501 if ( $cust_bill_pkg->pkgnum > 0 ) {
1503 if ( $cust_bill_pkg->setup != 0 ) {
1504 my $description = $desc;
1505 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1506 push @buf, [ $description,
1507 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1509 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1510 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1513 if ( $cust_bill_pkg->recur != 0 ) {
1516 ( $conf->exists('disable_line_item_date_ranges')
1518 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1519 time2str("%x", $cust_bill_pkg->edate) . ")"
1521 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1524 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1525 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1526 $cust_bill_pkg->sdate );
1529 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1531 } else { #pkgnum tax or one-shot line item
1533 if ( $cust_bill_pkg->setup != 0 ) {
1535 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1537 if ( $cust_bill_pkg->recur != 0 ) {
1538 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1539 . time2str("%x", $cust_bill_pkg->edate). ")",
1540 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1548 push @buf,['','-----------'];
1549 push @buf,['Total New Charges',
1550 $money_char. sprintf("%10.2f",$self->charged) ];
1553 push @buf,['','-----------'];
1554 push @buf,['Total Charges',
1555 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1559 foreach ( $self->cust_credited ) {
1561 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1563 my $reason = substr($_->cust_credit->reason,0,32);
1564 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1565 $reason = " ($reason) " if $reason;
1567 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1569 $money_char. sprintf("%10.2f",$_->amount)
1572 #foreach ( @cr_cust_credit ) {
1574 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1575 # $money_char. sprintf("%10.2f",$_->credited)
1579 #get & print payments
1580 foreach ( $self->cust_bill_pay ) {
1582 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1585 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1586 $money_char. sprintf("%10.2f",$_->amount )
1591 my $balance_due_msg = $self->balance_due_msg;
1593 push @buf,['','-----------'];
1594 push @buf,[$balance_due_msg, $money_char.
1595 sprintf("%10.2f", $balance_due ) ];
1597 #create the template
1598 $template ||= $self->_agent_template;
1599 my $templatefile = 'invoice_template';
1600 $templatefile .= "_$template" if length($template);
1601 my @invoice_template = $conf->config($templatefile)
1602 or die "cannot load config file $templatefile";
1605 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1606 /invoice_lines\((\d*)\)/;
1607 $invoice_lines += $1 || scalar(@buf);
1610 die "no invoice_lines() functions in template?" unless $wasfunc;
1611 my $invoice_template = new Text::Template (
1613 SOURCE => [ map "$_\n", @invoice_template ],
1614 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1615 $invoice_template->compile()
1616 or die "can't compile template: $Text::Template::ERROR";
1618 #setup template variables
1619 package FS::cust_bill::_template; #!
1620 use vars qw( $custnum $invnum $date $agent @address $overdue
1621 $page $total_pages @buf );
1623 $custnum = $self->custnum;
1624 $invnum = $self->invnum;
1625 $date = $self->_date;
1626 $agent = $self->cust_main->agent->agent;
1629 if ( $FS::cust_bill::invoice_lines ) {
1631 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1633 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1638 #format address (variable for the template)
1640 @address = ( '', '', '', '', '', '' );
1641 package FS::cust_bill; #!
1642 $FS::cust_bill::_template::address[$l++] =
1643 $cust_main->payname.
1644 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1645 ? " (P.O. #". $cust_main->payinfo. ")"
1649 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1650 if $cust_main->company;
1651 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1652 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1653 if $cust_main->address2;
1654 $FS::cust_bill::_template::address[$l++] =
1655 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1657 my $countrydefault = $conf->config('countrydefault') || 'US';
1658 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1659 unless $cust_main->country eq $countrydefault;
1661 # #overdue? (variable for the template)
1662 # $FS::cust_bill::_template::overdue = (
1664 # && $today > $self->_date
1665 ## && $self->printed > 1
1666 # && $self->printed > 0
1669 #and subroutine for the template
1670 sub FS::cust_bill::_template::invoice_lines {
1671 my $lines = shift || scalar(@buf);
1673 scalar(@buf) ? shift @buf : [ '', '' ];
1679 $FS::cust_bill::_template::page = 1;
1683 push @collect, split("\n",
1684 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1686 $FS::cust_bill::_template::page++;
1689 map "$_\n", @collect;
1693 =item print_latex [ TIME [ , TEMPLATE ] ]
1695 Internal method - returns a filename of a filled-in LaTeX template for this
1696 invoice (Note: add ".tex" to get the actual filename).
1698 See print_ps and print_pdf for methods that return PostScript and PDF output.
1700 TIME an optional value used to control the printing of overdue messages. The
1701 default is now. It isn't the date of the invoice; that's the `_date' field.
1702 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1703 L<Time::Local> and L<Date::Parse> for conversion functions.
1707 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1710 my( $self, $today, $template ) = @_;
1712 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1715 my $cust_main = $self->cust_main;
1716 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1717 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1719 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1720 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1721 #my $balance_due = $self->owed + $pr_total - $cr_total;
1722 my $balance_due = $self->owed + $pr_total;
1724 #create the template
1725 $template ||= $self->_agent_template;
1726 my $templatefile = 'invoice_latex';
1727 my $suffix = length($template) ? "_$template" : '';
1728 $templatefile .= $suffix;
1729 my @invoice_template = map "$_\n", $conf->config($templatefile)
1730 or die "cannot load config file $templatefile";
1732 my($format, $text_template);
1733 if ( grep { /^%%Detail/ } @invoice_template ) {
1734 #change this to a die when the old code is removed
1735 warn "old-style invoice template $templatefile; ".
1736 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1739 $format = 'Text::Template';
1740 $text_template = new Text::Template(
1742 SOURCE => \@invoice_template,
1743 DELIMITERS => [ '[@--', '--@]' ],
1746 $text_template->compile()
1747 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1751 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1752 $returnaddress = join("\n",
1753 $conf->config_orbase('invoice_latexreturnaddress', $template)
1756 $returnaddress = '~';
1759 my %invoice_data = (
1760 'custnum' => $self->custnum,
1761 'invnum' => $self->invnum,
1762 'date' => time2str('%b %o, %Y', $self->_date),
1763 'today' => time2str('%b %o, %Y', $today),
1764 'agent' => _latex_escape($cust_main->agent->agent),
1765 'payname' => _latex_escape($cust_main->payname),
1766 'company' => _latex_escape($cust_main->company),
1767 'address1' => _latex_escape($cust_main->address1),
1768 'address2' => _latex_escape($cust_main->address2),
1769 'city' => _latex_escape($cust_main->city),
1770 'state' => _latex_escape($cust_main->state),
1771 'zip' => _latex_escape($cust_main->zip),
1772 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1773 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1774 'returnaddress' => $returnaddress,
1776 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1777 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1778 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1781 my $countrydefault = $conf->config('countrydefault') || 'US';
1782 if ( $cust_main->country eq $countrydefault ) {
1783 $invoice_data{'country'} = '';
1785 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1788 $invoice_data{'notes'} =
1790 # #do variable substitutions in notes
1791 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1792 $conf->config_orbase('invoice_latexnotes', $template)
1794 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1797 $invoice_data{'footer'} =~ s/\n+$//;
1798 $invoice_data{'smallfooter'} =~ s/\n+$//;
1799 $invoice_data{'notes'} =~ s/\n+$//;
1801 $invoice_data{'po_line'} =
1802 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1803 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1807 if ( $format eq 'old' ) {
1810 my @total_item = ();
1811 while ( @invoice_template ) {
1812 my $line = shift @invoice_template;
1814 if ( $line =~ /^%%Detail\s*$/ ) {
1816 while ( ( my $line_item_line = shift @invoice_template )
1817 !~ /^%%EndDetail\s*$/ ) {
1818 push @line_item, $line_item_line;
1820 foreach my $line_item ( $self->_items ) {
1821 #foreach my $line_item ( $self->_items_pkg ) {
1822 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1823 $invoice_data{'description'} =
1824 _latex_escape($line_item->{'description'});
1825 if ( exists $line_item->{'ext_description'} ) {
1826 $invoice_data{'description'} .=
1827 "\\tabularnewline\n~~".
1828 join( "\\tabularnewline\n~~",
1829 map _latex_escape($_), @{$line_item->{'ext_description'}}
1832 $invoice_data{'amount'} = $line_item->{'amount'};
1833 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1835 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1838 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1840 while ( ( my $total_item_line = shift @invoice_template )
1841 !~ /^%%EndTotalDetails\s*$/ ) {
1842 push @total_item, $total_item_line;
1845 my @total_fill = ();
1848 foreach my $tax ( $self->_items_tax ) {
1849 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1850 $taxtotal += $tax->{'amount'};
1851 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1853 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1858 $invoice_data{'total_item'} = 'Sub-total';
1859 $invoice_data{'total_amount'} =
1860 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1861 unshift @total_fill,
1862 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1866 $invoice_data{'total_item'} = '\textbf{Total}';
1867 $invoice_data{'total_amount'} =
1868 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1870 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1873 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1876 foreach my $credit ( $self->_items_credits ) {
1877 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1879 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1881 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1886 foreach my $payment ( $self->_items_payments ) {
1887 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1889 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1891 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1895 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1896 $invoice_data{'total_amount'} =
1897 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1899 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1902 push @filled_in, @total_fill;
1905 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1906 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1907 push @filled_in, $line;
1918 } elsif ( $format eq 'Text::Template' ) {
1920 my @detail_items = ();
1921 my @total_items = ();
1923 $invoice_data{'detail_items'} = \@detail_items;
1924 $invoice_data{'total_items'} = \@total_items;
1926 foreach my $line_item ( $self->_items ) {
1928 ext_description => [],
1930 $detail->{'ref'} = $line_item->{'pkgnum'};
1931 $detail->{'quantity'} = 1;
1932 $detail->{'description'} = _latex_escape($line_item->{'description'});
1933 if ( exists $line_item->{'ext_description'} ) {
1934 @{$detail->{'ext_description'}} = map {
1936 } @{$line_item->{'ext_description'}};
1938 $detail->{'amount'} = $line_item->{'amount'};
1939 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1941 push @detail_items, $detail;
1946 foreach my $tax ( $self->_items_tax ) {
1948 $total->{'total_item'} = _latex_escape($tax->{'description'});
1949 $taxtotal += $tax->{'amount'};
1950 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1951 push @total_items, $total;
1956 $total->{'total_item'} = 'Sub-total';
1957 $total->{'total_amount'} =
1958 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1959 unshift @total_items, $total;
1964 $total->{'total_item'} = '\textbf{Total}';
1965 $total->{'total_amount'} =
1966 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1967 push @total_items, $total;
1970 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1973 foreach my $credit ( $self->_items_credits ) {
1975 $total->{'total_item'} = _latex_escape($credit->{'description'});
1977 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1978 push @total_items, $total;
1982 foreach my $payment ( $self->_items_payments ) {
1984 $total->{'total_item'} = _latex_escape($payment->{'description'});
1986 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1987 push @total_items, $total;
1992 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1993 $total->{'total_amount'} =
1994 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1995 push @total_items, $total;
1999 die "guru meditation #54";
2002 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2003 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2007 ) or die "can't open temp file: $!\n";
2008 if ( $format eq 'old' ) {
2009 print $fh join('', @filled_in );
2010 } elsif ( $format eq 'Text::Template' ) {
2011 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2013 die "guru meditation #32";
2017 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2022 =item print_ps [ TIME [ , TEMPLATE ] ]
2024 Returns an postscript invoice, as a scalar.
2026 TIME an optional value used to control the printing of overdue messages. The
2027 default is now. It isn't the date of the invoice; that's the `_date' field.
2028 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2029 L<Time::Local> and L<Date::Parse> for conversion functions.
2036 my $file = $self->print_latex(@_);
2037 FS::Misc::generate_ps($file);
2041 =item print_pdf [ TIME [ , TEMPLATE ] ]
2043 Returns an PDF invoice, as a scalar.
2045 TIME an optional value used to control the printing of overdue messages. The
2046 default is now. It isn't the date of the invoice; that's the `_date' field.
2047 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2048 L<Time::Local> and L<Date::Parse> for conversion functions.
2055 my $file = $self->print_latex(@_);
2057 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2060 #system('pdflatex', "$file.tex");
2061 #system('pdflatex', "$file.tex");
2062 #! LaTeX Error: Unknown graphics extension: .eps.
2064 my $sfile = shell_quote $file;
2066 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2067 or die "pslatex $file.tex failed; see $file.log for details?\n";
2068 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2069 or die "pslatex $file.tex failed; see $file.log for details?\n";
2071 #system('dvipdf', "$file.dvi", "$file.pdf" );
2073 "dvips -q -t letter -f $sfile.dvi ".
2074 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2077 or die "dvips | gs failed: $!";
2079 open(PDF, "<$file.pdf")
2080 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2082 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2095 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2097 Returns an HTML invoice, as a scalar.
2099 TIME an optional value used to control the printing of overdue messages. The
2100 default is now. It isn't the date of the invoice; that's the `_date' field.
2101 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2102 L<Time::Local> and L<Date::Parse> for conversion functions.
2104 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2105 when emailing the invoice as part of a multipart/related MIME email.
2109 #some falze laziness w/print_text and print_latex (and send_csv)
2111 my( $self, $today, $template, $cid ) = @_;
2114 my $cust_main = $self->cust_main;
2115 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2116 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2118 $template ||= $self->_agent_template;
2119 my $templatefile = 'invoice_html';
2120 my $suffix = length($template) ? "_$template" : '';
2121 $templatefile .= $suffix;
2122 my @html_template = map "$_\n", $conf->config($templatefile)
2123 or die "cannot load config file $templatefile";
2125 my $html_template = new Text::Template(
2127 SOURCE => \@html_template,
2128 DELIMITERS => [ '<%=', '%>' ],
2131 $html_template->compile()
2132 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2134 my %invoice_data = (
2135 'custnum' => $self->custnum,
2136 'invnum' => $self->invnum,
2137 'date' => time2str('%b %o, %Y', $self->_date),
2138 'today' => time2str('%b %o, %Y', $today),
2139 'agent' => encode_entities($cust_main->agent->agent),
2140 'payname' => encode_entities($cust_main->payname),
2141 'company' => encode_entities($cust_main->company),
2142 'address1' => encode_entities($cust_main->address1),
2143 'address2' => encode_entities($cust_main->address2),
2144 'city' => encode_entities($cust_main->city),
2145 'state' => encode_entities($cust_main->state),
2146 'zip' => encode_entities($cust_main->zip),
2147 'terms' => $conf->config('invoice_default_terms')
2148 || 'Payable upon receipt',
2150 'template' => $template,
2151 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2155 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2156 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2158 $invoice_data{'returnaddress'} =
2159 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2161 $invoice_data{'returnaddress'} =
2164 s/\\\\\*?\s*$/<BR>/;
2165 s/\\hyphenation\{[\w\s\-]+\}//;
2168 $conf->config_orbase( 'invoice_latexreturnaddress',
2174 my $countrydefault = $conf->config('countrydefault') || 'US';
2175 if ( $cust_main->country eq $countrydefault ) {
2176 $invoice_data{'country'} = '';
2178 $invoice_data{'country'} =
2179 encode_entities(code2country($cust_main->country));
2183 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2184 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2186 $invoice_data{'notes'} =
2187 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2189 $invoice_data{'notes'} =
2191 s/%%(.*)$/<!-- $1 -->/;
2192 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2193 s/\\begin\{enumerate\}/<ol>/;
2195 s/\\end\{enumerate\}/<\/ol>/;
2196 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2200 $conf->config_orbase('invoice_latexnotes', $template)
2204 # #do variable substitutions in notes
2205 # $invoice_data{'notes'} =
2207 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2208 # $conf->config_orbase('invoice_latexnotes', $suffix)
2212 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2213 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2215 $invoice_data{'footer'} =
2216 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2218 $invoice_data{'footer'} =
2219 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2220 $conf->config_orbase('invoice_latexfooter', $template)
2224 $invoice_data{'po_line'} =
2225 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2226 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2229 my $money_char = $conf->config('money_char') || '$';
2231 foreach my $line_item ( $self->_items ) {
2233 ext_description => [],
2235 $detail->{'ref'} = $line_item->{'pkgnum'};
2236 $detail->{'description'} = encode_entities($line_item->{'description'});
2237 if ( exists $line_item->{'ext_description'} ) {
2238 @{$detail->{'ext_description'}} = map {
2239 encode_entities($_);
2240 } @{$line_item->{'ext_description'}};
2242 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2243 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2245 push @{$invoice_data{'detail_items'}}, $detail;
2250 foreach my $tax ( $self->_items_tax ) {
2252 $total->{'total_item'} = encode_entities($tax->{'description'});
2253 $taxtotal += $tax->{'amount'};
2254 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2255 push @{$invoice_data{'total_items'}}, $total;
2260 $total->{'total_item'} = 'Sub-total';
2261 $total->{'total_amount'} =
2262 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2263 unshift @{$invoice_data{'total_items'}}, $total;
2266 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2269 $total->{'total_item'} = '<b>Total</b>';
2270 $total->{'total_amount'} =
2271 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2272 push @{$invoice_data{'total_items'}}, $total;
2275 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2278 foreach my $credit ( $self->_items_credits ) {
2280 $total->{'total_item'} = encode_entities($credit->{'description'});
2282 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2283 push @{$invoice_data{'total_items'}}, $total;
2287 foreach my $payment ( $self->_items_payments ) {
2289 $total->{'total_item'} = encode_entities($payment->{'description'});
2291 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2292 push @{$invoice_data{'total_items'}}, $total;
2297 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2298 $total->{'total_amount'} =
2299 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2300 push @{$invoice_data{'total_items'}}, $total;
2303 $html_template->fill_in( HASH => \%invoice_data);
2306 # quick subroutine for print_latex
2308 # There are ten characters that LaTeX treats as special characters, which
2309 # means that they do not simply typeset themselves:
2310 # # $ % & ~ _ ^ \ { }
2312 # TeX ignores blanks following an escaped character; if you want a blank (as
2313 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2317 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2318 $value =~ s/([<>])/\$$1\$/g;
2322 #utility methods for print_*
2324 sub balance_due_msg {
2326 my $msg = 'Balance Due';
2327 return $msg unless $conf->exists('invoice_default_terms');
2328 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2329 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2330 } elsif ( $conf->config('invoice_default_terms') ) {
2331 $msg .= ' - '. $conf->config('invoice_default_terms');
2338 my @display = scalar(@_)
2340 : qw( _items_previous _items_pkg );
2341 #: qw( _items_pkg );
2342 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2344 foreach my $display ( @display ) {
2345 push @b, $self->$display(@_);
2350 sub _items_previous {
2352 my $cust_main = $self->cust_main;
2353 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2355 foreach ( @pr_cust_bill ) {
2357 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2358 ' ('. time2str('%x',$_->_date). ')',
2359 #'pkgpart' => 'N/A',
2361 'amount' => sprintf("%.2f", $_->owed),
2367 # 'description' => 'Previous Balance',
2368 # #'pkgpart' => 'N/A',
2369 # 'pkgnum' => 'N/A',
2370 # 'amount' => sprintf("%10.2f", $pr_total ),
2371 # 'ext_description' => [ map {
2372 # "Invoice ". $_->invnum.
2373 # " (". time2str("%x",$_->_date). ") ".
2374 # sprintf("%10.2f", $_->owed)
2375 # } @pr_cust_bill ],
2382 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2383 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2388 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2389 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2392 sub _items_cust_bill_pkg {
2394 my $cust_bill_pkg = shift;
2397 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2399 my $desc = $cust_bill_pkg->desc;
2401 if ( $cust_bill_pkg->pkgnum > 0 ) {
2403 if ( $cust_bill_pkg->setup != 0 ) {
2404 my $description = $desc;
2405 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2406 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2407 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2409 description => $description,
2410 #pkgpart => $part_pkg->pkgpart,
2411 pkgnum => $cust_bill_pkg->pkgnum,
2412 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2413 ext_description => \@d,
2417 if ( $cust_bill_pkg->recur != 0 ) {
2419 description => $desc .
2420 ( $conf->exists('disable_line_item_date_ranges')
2422 : " (" .time2str("%x", $cust_bill_pkg->sdate).
2423 " - ".time2str("%x", $cust_bill_pkg->edate).")"
2425 #pkgpart => $part_pkg->pkgpart,
2426 pkgnum => $cust_bill_pkg->pkgnum,
2427 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2429 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2430 $cust_bill_pkg->sdate),
2431 $cust_bill_pkg->details,
2436 } else { #pkgnum tax or one-shot line item (??)
2438 if ( $cust_bill_pkg->setup != 0 ) {
2440 'description' => $desc,
2441 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2444 if ( $cust_bill_pkg->recur != 0 ) {
2446 'description' => "$desc (".
2447 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2448 time2str("%x", $cust_bill_pkg->edate). ')',
2449 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2461 sub _items_credits {
2466 foreach ( $self->cust_credited ) {
2468 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2470 my $reason = $_->cust_credit->reason;
2471 #my $reason = substr($_->cust_credit->reason,0,32);
2472 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2473 $reason = " ($reason) " if $reason;
2475 #'description' => 'Credit ref\#'. $_->crednum.
2476 # " (". time2str("%x",$_->cust_credit->_date) .")".
2478 'description' => 'Credit applied '.
2479 time2str("%x",$_->cust_credit->_date). $reason,
2480 'amount' => sprintf("%.2f",$_->amount),
2483 #foreach ( @cr_cust_credit ) {
2485 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2486 # $money_char. sprintf("%10.2f",$_->credited)
2494 sub _items_payments {
2498 #get & print payments
2499 foreach ( $self->cust_bill_pay ) {
2501 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2504 'description' => "Payment received ".
2505 time2str("%x",$_->cust_pay->_date ),
2506 'amount' => sprintf("%.2f", $_->amount )
2525 sub process_reprint {
2526 process_re_X('print', @_);
2533 sub process_reemail {
2534 process_re_X('email', @_);
2542 process_re_X('fax', @_);
2545 use Storable qw(thaw);
2549 my( $method, $job ) = ( shift, shift );
2550 warn "process_re_X $method for job $job\n" if $DEBUG;
2552 my $param = thaw(decode_base64(shift));
2553 warn Dumper($param) if $DEBUG;
2564 my($method, $job, %param ) = @_;
2565 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2567 warn "re_X $method for job $job with param:\n".
2568 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2571 #some false laziness w/search/cust_bill.html
2573 my $orderby = 'ORDER BY cust_bill._date';
2577 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2578 push @where, "cust_bill._date >= $1";
2580 if ( $param{'end'} =~ /^(\d+)$/ ) {
2581 push @where, "cust_bill._date < $1";
2583 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2584 push @where, "cust_main.agentnum = $1";
2588 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2589 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2590 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2591 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2593 push @where, "0 != $owed"
2596 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2599 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2601 my $addl_from = 'left join cust_main using ( custnum )';
2603 if ( $param{'newest_percust'} ) {
2604 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2605 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2606 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2609 my @cust_bill = qsearch( 'cust_bill',
2611 "$distinct cust_bill.*",
2617 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2618 foreach my $cust_bill ( @cust_bill ) {
2619 $cust_bill->$method();
2621 if ( $job ) { #progressbar foo
2623 if ( time - $min_sec > $last ) {
2624 my $error = $job->update_statustext(
2625 int( 100 * $num / scalar(@cust_bill) )
2627 die $error if $error;
2642 print_text formatting (and some logic :/) is in source, but needs to be
2643 slurped in from a file. Also number of lines ($=).
2647 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2648 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base