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 open_cust_bill_pkg
230 Returns the open line items for this invoice.
232 Note that cust_bill_pkg with both setup and recur fees are returned as two
233 separate line items, each with only one fee.
237 # modeled after cust_main::open_cust_bill
238 sub open_cust_bill_pkg {
241 # grep { $_->owed > 0 } $self->cust_bill_pkg
243 my %other = ( 'recur' => 'setup',
244 'setup' => 'recur', );
246 foreach my $field ( qw( recur setup )) {
247 push @open, map { $_->set( $other{$field}, 0 ); $_; }
248 grep { $_->owed($field) > 0 }
249 $self->cust_bill_pkg;
255 =item cust_bill_event
257 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
262 sub cust_bill_event {
264 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
270 Returns the customer (see L<FS::cust_main>) for this invoice.
276 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
279 =item cust_suspend_if_balance_over AMOUNT
281 Suspends the customer associated with this invoice if the total amount owed on
282 this invoice and all older invoices is greater than the specified amount.
284 Returns a list: an empty list on success or a list of errors.
288 sub cust_suspend_if_balance_over {
289 my( $self, $amount ) = ( shift, shift );
290 my $cust_main = $self->cust_main;
291 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
300 Depreciated. See the cust_credited method.
302 #Returns a list consisting of the total previous credited (see
303 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
304 #outstanding credits (FS::cust_credit objects).
310 croak "FS::cust_bill->cust_credit depreciated; see ".
311 "FS::cust_bill->cust_credit_bill";
314 #my @cust_credit = sort { $a->_date <=> $b->_date }
315 # grep { $_->credited != 0 && $_->_date < $self->_date }
316 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
318 #foreach (@cust_credit) { $total += $_->credited; }
319 #$total, @cust_credit;
324 Depreciated. See the cust_bill_pay method.
326 #Returns all payments (see L<FS::cust_pay>) for this invoice.
332 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
334 #sort { $a->_date <=> $b->_date }
335 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
341 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
347 sort { $a->_date <=> $b->_date }
348 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
353 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
359 sort { $a->_date <=> $b->_date }
360 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
366 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
373 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
375 foreach (@taxlines) { $total += $_->setup; }
381 Returns the amount owed (still outstanding) on this invoice, which is charged
382 minus all payment applications (see L<FS::cust_bill_pay>) and credit
383 applications (see L<FS::cust_credit_bill>).
389 my $balance = $self->charged;
390 $balance -= $_->amount foreach ( $self->cust_bill_pay );
391 $balance -= $_->amount foreach ( $self->cust_credited );
392 $balance = sprintf( "%.2f", $balance);
393 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
398 =item generate_email PARAMHASH
400 PARAMHASH can contain the following:
404 =item from => sender address, required
406 =item tempate => alternate template name, optional
408 =item print_text => text attachment arrayref, optional
410 =item subject => email subject, optional
414 Returns an argument list to be passed to L<FS::Misc::send_email>.
425 my $me = '[FS::cust_bill::generate_email]';
428 'from' => $args{'from'},
429 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
432 if (ref($args{'to'} eq 'ARRAY')) {
433 $return{'to'} = $args{'to'};
435 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
436 $self->cust_main->invoicing_list
440 if ( $conf->exists('invoice_html') ) {
442 warn "$me creating HTML/text multipart message"
445 $return{'nobody'} = 1;
447 my $alternative = build MIME::Entity
448 'Type' => 'multipart/alternative',
449 'Encoding' => '7bit',
450 'Disposition' => 'inline'
454 if ( $conf->exists('invoice_email_pdf')
455 and scalar($conf->config('invoice_email_pdf_note')) ) {
457 warn "$me using 'invoice_email_pdf_note' in multipart message"
459 $data = [ map { $_ . "\n" }
460 $conf->config('invoice_email_pdf_note')
465 warn "$me not using 'invoice_email_pdf_note' in multipart message"
467 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
468 $data = $args{'print_text'};
470 $data = [ $self->print_text('', $args{'template'}) ];
475 $alternative->attach(
476 'Type' => 'text/plain',
477 #'Encoding' => 'quoted-printable',
478 'Encoding' => '7bit',
480 'Disposition' => 'inline',
483 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
484 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
486 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
488 if ( defined($args{'_template'}) && length($args{'_template'})
489 && -e "$path/logo_". $args{'_template'}. ".png"
492 $file = "$path/logo_". $args{'_template'}. ".png";
494 $file = "$path/logo.png";
497 my $image = build MIME::Entity
498 'Type' => 'image/png',
499 'Encoding' => 'base64',
501 'Filename' => 'logo.png',
502 'Content-ID' => "<$content_id>",
505 $alternative->attach(
506 'Type' => 'text/html',
507 'Encoding' => 'quoted-printable',
508 'Data' => [ '<html>',
511 ' '. encode_entities($return{'subject'}),
514 ' <body bgcolor="#e8e8e8">',
515 $self->print_html('', $args{'template'}, $content_id),
519 'Disposition' => 'inline',
520 #'Filename' => 'invoice.pdf',
523 if ( $conf->exists('invoice_email_pdf') ) {
528 # multipart/alternative
534 my $related = build MIME::Entity 'Type' => 'multipart/related',
535 'Encoding' => '7bit';
537 #false laziness w/Misc::send_email
538 $related->head->replace('Content-type',
540 '; boundary="'. $related->head->multipart_boundary. '"'.
541 '; type=multipart/alternative'
544 $related->add_part($alternative);
546 $related->add_part($image);
548 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
550 $return{'mimeparts'} = [ $related, $pdf ];
554 #no other attachment:
556 # multipart/alternative
561 $return{'content-type'} = 'multipart/related';
562 $return{'mimeparts'} = [ $alternative, $image ];
563 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
564 #$return{'disposition'} = 'inline';
570 if ( $conf->exists('invoice_email_pdf') ) {
571 warn "$me creating PDF attachment"
574 #mime parts arguments a la MIME::Entity->build().
575 $return{'mimeparts'} = [
576 { $self->mimebuild_pdf('', $args{'template'}) }
580 if ( $conf->exists('invoice_email_pdf')
581 and scalar($conf->config('invoice_email_pdf_note')) ) {
583 warn "$me using 'invoice_email_pdf_note'"
585 $return{'body'} = [ map { $_ . "\n" }
586 $conf->config('invoice_email_pdf_note')
591 warn "$me not using 'invoice_email_pdf_note'"
593 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
594 $return{'body'} = $args{'print_text'};
596 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
609 Returns a list suitable for passing to MIME::Entity->build(), representing
610 this invoice as PDF attachment.
617 'Type' => 'application/pdf',
618 'Encoding' => 'base64',
619 'Data' => [ $self->print_pdf(@_) ],
620 'Disposition' => 'attachment',
621 'Filename' => 'invoice.pdf',
625 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
627 Sends this invoice to the destinations configured for this customer: sends
628 email, prints and/or faxes. See L<FS::cust_main_invoice>.
630 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
632 AGENTNUM, if specified, means that this invoice will only be sent for customers
633 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
634 single agent) or an arrayref of agentnums.
636 INVOICE_FROM, if specified, overrides the default email invoice From: address.
642 my $template = scalar(@_) ? shift : '';
643 if ( scalar(@_) && $_[0] ) {
644 my $agentnums = ref($_[0]) ? shift : [ shift ];
645 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
651 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
653 my @invoicing_list = $self->cust_main->invoicing_list;
655 $self->email($template, $invoice_from)
656 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
658 $self->print($template)
659 if grep { $_ eq 'POST' } @invoicing_list; #postal
661 $self->fax($template)
662 if grep { $_ eq 'FAX' } @invoicing_list; #fax
668 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
672 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
674 INVOICE_FROM, if specified, overrides the default email invoice From: address.
680 my $template = scalar(@_) ? shift : '';
684 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
686 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
687 $self->cust_main->invoicing_list;
689 #better to notify this person than silence
690 @invoicing_list = ($invoice_from) unless @invoicing_list;
692 my $error = send_email(
693 $self->generate_email(
694 'from' => $invoice_from,
695 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
696 'template' => $template,
699 die "can't email invoice: $error\n" if $error;
700 #die "$error\n" if $error;
704 =item lpr_data [ TEMPLATENAME ]
706 Returns the postscript or plaintext for this invoice as an arrayref.
708 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
713 my( $self, $template) = @_;
714 $conf->exists('invoice_latex')
715 ? [ $self->print_ps('', $template) ]
716 : [ $self->print_text('', $template) ];
719 =item print [ TEMPLATENAME ]
723 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
729 my $template = scalar(@_) ? shift : '';
731 my $lpr = $conf->config('lpr');
734 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
736 $outerr = ": $outerr" if length($outerr);
737 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
742 =item fax [ TEMPLATENAME ]
746 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
752 my $template = scalar(@_) ? shift : '';
754 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
755 unless $conf->exists('invoice_latex');
757 my $dialstring = $self->cust_main->getfield('fax');
760 my $error = send_fax( 'docdata' => $self->lpr_data($template),
761 'dialstring' => $dialstring,
763 die $error if $error;
767 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
769 Like B<send>, but only sends the invoice if it is the newest open invoice for
779 grep { $_->owed > 0 }
780 qsearch('cust_bill', {
781 'custnum' => $self->custnum,
782 #'_date' => { op=>'>', value=>$self->_date },
783 'invnum' => { op=>'>', value=>$self->invnum },
790 =item send_csv OPTION => VALUE, ...
792 Sends invoice as a CSV data-file to a remote host with the specified protocol.
796 protocol - currently only "ftp"
802 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
803 and YYMMDDHHMMSS is a timestamp.
805 See L</print_csv> for a description of the output format.
810 my($self, %opt) = @_;
814 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
815 mkdir $spooldir, 0700 unless -d $spooldir;
817 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
818 my $file = "$spooldir/$tracctnum.csv";
820 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
822 open(CSV, ">$file") or die "can't open $file: $!";
830 if ( $opt{protocol} eq 'ftp' ) {
831 eval "use Net::FTP;";
833 $net = Net::FTP->new($opt{server}) or die @$;
835 die "unknown protocol: $opt{protocol}";
838 $net->login( $opt{username}, $opt{password} )
839 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
841 $net->binary or die "can't set binary mode";
843 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
845 $net->put($file) or die "can't put $file: $!";
855 Spools CSV invoice data.
861 =item format - 'default' or 'billco'
863 =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>).
865 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
867 =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.
874 my($self, %opt) = @_;
876 my $cust_main = $self->cust_main;
878 if ( $opt{'dest'} ) {
879 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
880 $cust_main->invoicing_list;
881 return 'N/A' unless $invoicing_list{$opt{'dest'}}
882 || ! keys %invoicing_list;
885 if ( $opt{'balanceover'} ) {
887 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
890 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
891 mkdir $spooldir, 0700 unless -d $spooldir;
893 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
897 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
898 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
901 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
903 open(CSV, ">>$file") or die "can't open $file: $!";
909 if ( lc($opt{'format'}) eq 'billco' ) {
916 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
919 open(CSV,">>$file") or die "can't open $file: $!";
933 =item print_csv OPTION => VALUE, ...
935 Returns CSV data for this invoice.
939 format - 'default' or 'billco'
941 Returns a list consisting of two scalars. The first is a single line of CSV
942 header information for this invoice. The second is one or more lines of CSV
943 detail information for this invoice.
945 If I<format> is not specified or "default", the fields of the CSV file are as
948 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
952 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
954 B<record_type> is C<cust_bill> for the initial header line only. The
955 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
956 fields are filled in.
958 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
959 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
962 =item invnum - invoice number
964 =item custnum - customer number
966 =item _date - invoice date
968 =item charged - total invoice amount
970 =item first - customer first name
972 =item last - customer first name
974 =item company - company name
976 =item address1 - address line 1
978 =item address2 - address line 1
988 =item pkg - line item description
990 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
992 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
994 =item sdate - start date for recurring fee
996 =item edate - end date for recurring fee
1000 If I<format> is "billco", the fields of the header CSV file are as follows:
1002 +-------------------------------------------------------------------+
1003 | FORMAT HEADER FILE |
1004 |-------------------------------------------------------------------|
1005 | Field | Description | Name | Type | Width |
1006 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1007 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1008 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1009 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1010 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1011 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1012 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1013 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1014 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1015 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1016 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1017 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1018 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1019 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1020 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1021 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1022 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1023 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1024 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1025 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1026 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1027 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1028 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1029 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1030 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1031 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1032 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1033 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1034 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1035 +-------+-------------------------------+------------+------+-------+
1037 If I<format> is "billco", the fields of the detail CSV file are as follows:
1039 FORMAT FOR DETAIL FILE
1041 Field | Description | Name | Type | Width
1042 1 | N/A-Leave Empty | RC | CHAR | 2
1043 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1044 3 | Account Number | TRACCTNUM | CHAR | 15
1045 4 | Invoice Number | TRINVOICE | CHAR | 15
1046 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1047 6 | Transaction Detail | DETAILS | CHAR | 100
1048 7 | Amount | AMT | NUM* | 9
1049 8 | Line Format Control** | LNCTRL | CHAR | 2
1050 9 | Grouping Code | GROUP | CHAR | 2
1051 10 | User Defined | ACCT CODE | CHAR | 15
1056 my($self, %opt) = @_;
1058 eval "use Text::CSV_XS";
1061 my $cust_main = $self->cust_main;
1063 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1065 if ( lc($opt{'format'}) eq 'billco' ) {
1068 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1071 if ( $conf->exists('invoice_default_terms')
1072 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1073 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1076 my( $previous_balance, @unused ) = $self->previous; #previous balance
1078 my $pmt_cr_applied = 0;
1079 $pmt_cr_applied += $_->{'amount'}
1080 foreach ( $self->_items_payments, $self->_items_credits ) ;
1082 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1085 '', # 1 | N/A-Leave Empty CHAR 2
1086 '', # 2 | N/A-Leave Empty CHAR 15
1087 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1088 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1089 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1090 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1091 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1092 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1093 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1094 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1095 '', # 10 | Ancillary Billing Information CHAR 30
1096 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1097 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1100 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1103 $duedate, # 14 | Bill Due Date CHAR 10
1105 $previous_balance, # 15 | Previous Balance NUM* 9
1106 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1107 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1108 $totaldue, # 18 | Total Amt Due NUM* 9
1109 $totaldue, # 19 | Total Amt Due NUM* 9
1110 '', # 20 | 30 Day Aging NUM* 9
1111 '', # 21 | 60 Day Aging NUM* 9
1112 '', # 22 | 90 Day Aging NUM* 9
1113 'N', # 23 | Y/N CHAR 1
1114 '', # 24 | Remittance automation CHAR 100
1115 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1116 $self->custnum, # 26 | Customer Reference Number CHAR 15
1117 '0', # 27 | Federal Tax*** NUM* 9
1118 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1119 '0', # 29 | Other Taxes & Fees*** NUM* 9
1128 time2str("%x", $self->_date),
1129 sprintf("%.2f", $self->charged),
1130 ( map { $cust_main->getfield($_) }
1131 qw( first last company address1 address2 city state zip country ) ),
1133 ) or die "can't create csv";
1136 my $header = $csv->string. "\n";
1139 if ( lc($opt{'format'}) eq 'billco' ) {
1142 foreach my $item ( $self->_items_pkg ) {
1145 '', # 1 | N/A-Leave Empty CHAR 2
1146 '', # 2 | N/A-Leave Empty CHAR 15
1147 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1148 $self->invnum, # 4 | Invoice Number CHAR 15
1149 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1150 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1151 $item->{'amount'}, # 7 | Amount NUM* 9
1152 '', # 8 | Line Format Control** CHAR 2
1153 '', # 9 | Grouping Code CHAR 2
1154 '', # 10 | User Defined CHAR 15
1157 $detail .= $csv->string. "\n";
1163 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1165 my($pkg, $setup, $recur, $sdate, $edate);
1166 if ( $cust_bill_pkg->pkgnum ) {
1168 ($pkg, $setup, $recur, $sdate, $edate) = (
1169 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1170 ( $cust_bill_pkg->setup != 0
1171 ? sprintf("%.2f", $cust_bill_pkg->setup )
1173 ( $cust_bill_pkg->recur != 0
1174 ? sprintf("%.2f", $cust_bill_pkg->recur )
1176 ( $cust_bill_pkg->sdate
1177 ? time2str("%x", $cust_bill_pkg->sdate)
1179 ($cust_bill_pkg->edate
1180 ?time2str("%x", $cust_bill_pkg->edate)
1184 } else { #pkgnum tax
1185 next unless $cust_bill_pkg->setup != 0;
1186 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1187 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1189 ($pkg, $setup, $recur, $sdate, $edate) =
1190 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1196 ( map { '' } (1..11) ),
1197 ($pkg, $setup, $recur, $sdate, $edate)
1198 ) or die "can't create csv";
1200 $detail .= $csv->string. "\n";
1206 ( $header, $detail );
1212 Pays this invoice with a compliemntary payment. If there is an error,
1213 returns the error, otherwise returns false.
1219 my $cust_pay = new FS::cust_pay ( {
1220 'invnum' => $self->invnum,
1221 'paid' => $self->owed,
1224 'payinfo' => $self->cust_main->payinfo,
1232 Attempts to pay this invoice with a credit card payment via a
1233 Business::OnlinePayment realtime gateway. See
1234 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1235 for supported processors.
1241 $self->realtime_bop( 'CC', @_ );
1246 Attempts to pay this invoice with an electronic check (ACH) payment via a
1247 Business::OnlinePayment realtime gateway. See
1248 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1249 for supported processors.
1255 $self->realtime_bop( 'ECHECK', @_ );
1260 Attempts to pay this invoice with phone bill (LEC) payment via a
1261 Business::OnlinePayment realtime gateway. See
1262 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1263 for supported processors.
1269 $self->realtime_bop( 'LEC', @_ );
1273 my( $self, $method ) = @_;
1275 my $cust_main = $self->cust_main;
1276 my $balance = $cust_main->balance;
1277 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1278 $amount = sprintf("%.2f", $amount);
1279 return "not run (balance $balance)" unless $amount > 0;
1281 my $description = 'Internet Services';
1282 if ( $conf->exists('business-onlinepayment-description') ) {
1283 my $dtempl = $conf->config('business-onlinepayment-description');
1285 my $agent_obj = $cust_main->agent
1286 or die "can't retreive agent for $cust_main (agentnum ".
1287 $cust_main->agentnum. ")";
1288 my $agent = $agent_obj->agent;
1289 my $pkgs = join(', ',
1290 map { $_->cust_pkg->part_pkg->pkg }
1291 grep { $_->pkgnum } $self->cust_bill_pkg
1293 $description = eval qq("$dtempl");
1296 $cust_main->realtime_bop($method, $amount,
1297 'description' => $description,
1298 'invnum' => $self->invnum,
1305 Adds a payment for this invoice to the pending credit card batch (see
1306 L<FS::cust_pay_batch>).
1312 my $cust_main = $self->cust_main;
1314 my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1315 return '' unless $amount > 0;
1317 my $oldAutoCommit = $FS::UID::AutoCommit;
1318 local $FS::UID::AutoCommit = 0;
1321 my $pay_batch = qsearchs('pay_batch', {'status' => 'O'});
1323 unless ( $pay_batch ) {
1324 $pay_batch = new FS::pay_batch;
1325 $pay_batch->setfield('status' => 'O');
1326 my $error = $pay_batch->insert;
1328 $dbh->rollback if $oldAutoCommit;
1329 die "error creating new batch: $error\n";
1333 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1334 'batchnum' => $pay_batch->getfield('batchnum'),
1335 'custnum' => $cust_main->getfield('custnum'),
1338 my $cust_pay_batch = new FS::cust_pay_batch ( {
1339 'batchnum' => $pay_batch->getfield('batchnum'),
1340 'invnum' => $self->getfield('invnum'), # is there a better value?
1341 'custnum' => $cust_main->getfield('custnum'),
1342 'last' => $cust_main->getfield('last'),
1343 'first' => $cust_main->getfield('first'),
1344 'address1' => $cust_main->getfield('address1'),
1345 'address2' => $cust_main->getfield('address2'),
1346 'city' => $cust_main->getfield('city'),
1347 'state' => $cust_main->getfield('state'),
1348 'zip' => $cust_main->getfield('zip'),
1349 'country' => $cust_main->getfield('country'),
1350 'payby' => $cust_main->payby,
1351 'payinfo' => $cust_main->payinfo,
1352 'exp' => $cust_main->getfield('paydate'),
1353 'payname' => $cust_main->getfield('payname'),
1354 'amount' => $amount, # consolidating
1357 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1358 if $old_cust_pay_batch;
1361 if ($old_cust_pay_batch) {
1362 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1364 $error = $cust_pay_batch->insert;
1368 $dbh->rollback if $oldAutoCommit;
1372 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1376 sub _agent_template {
1378 $self->_agent_plandata('agent_templatename');
1381 sub _agent_invoice_from {
1383 $self->_agent_plandata('agent_invoice_from');
1386 sub _agent_plandata {
1387 my( $self, $option ) = @_;
1389 my $part_bill_event = qsearchs( 'part_bill_event',
1391 'payby' => $self->cust_main->payby,
1392 'plan' => 'send_agent',
1393 'plandata' => { 'op' => '~',
1394 'value' => "(^|\n)agentnum ".
1396 $self->cust_main->agentnum.
1402 'ORDER BY seconds LIMIT 1'
1405 return '' unless $part_bill_event;
1407 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1410 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1411 " plandata for $option";
1417 =item print_text [ TIME [ , TEMPLATE ] ]
1419 Returns an text invoice, as a list of lines.
1421 TIME an optional value used to control the printing of overdue messages. The
1422 default is now. It isn't the date of the invoice; that's the `_date' field.
1423 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1424 L<Time::Local> and L<Date::Parse> for conversion functions.
1428 #still some false laziness w/_items stuff (and send_csv)
1431 my( $self, $today, $template ) = @_;
1434 # my $invnum = $self->invnum;
1435 my $cust_main = $self->cust_main;
1436 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1437 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1439 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1440 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1441 #my $balance_due = $self->owed + $pr_total - $cr_total;
1442 my $balance_due = $self->owed + $pr_total;
1445 #my($description,$amount);
1449 foreach ( @pr_cust_bill ) {
1451 "Previous Balance, Invoice #". $_->invnum.
1452 " (". time2str("%x",$_->_date). ")",
1453 $money_char. sprintf("%10.2f",$_->owed)
1456 if (@pr_cust_bill) {
1457 push @buf,['','-----------'];
1458 push @buf,[ 'Total Previous Balance',
1459 $money_char. sprintf("%10.2f",$pr_total ) ];
1464 foreach my $cust_bill_pkg (
1465 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1466 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1469 my $desc = $cust_bill_pkg->desc;
1471 if ( $cust_bill_pkg->pkgnum > 0 ) {
1473 if ( $cust_bill_pkg->setup != 0 ) {
1474 my $description = $desc;
1475 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1476 push @buf, [ $description,
1477 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1479 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1480 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1483 if ( $cust_bill_pkg->recur != 0 ) {
1485 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1486 time2str("%x", $cust_bill_pkg->edate) . ")",
1487 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1490 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1491 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1492 $cust_bill_pkg->sdate );
1495 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1497 } else { #pkgnum tax or one-shot line item
1499 if ( $cust_bill_pkg->setup != 0 ) {
1501 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1503 if ( $cust_bill_pkg->recur != 0 ) {
1504 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1505 . time2str("%x", $cust_bill_pkg->edate). ")",
1506 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1514 push @buf,['','-----------'];
1515 push @buf,['Total New Charges',
1516 $money_char. sprintf("%10.2f",$self->charged) ];
1519 push @buf,['','-----------'];
1520 push @buf,['Total Charges',
1521 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1525 foreach ( $self->cust_credited ) {
1527 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1529 my $reason = substr($_->cust_credit->reason,0,32);
1530 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1531 $reason = " ($reason) " if $reason;
1533 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1535 $money_char. sprintf("%10.2f",$_->amount)
1538 #foreach ( @cr_cust_credit ) {
1540 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1541 # $money_char. sprintf("%10.2f",$_->credited)
1545 #get & print payments
1546 foreach ( $self->cust_bill_pay ) {
1548 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1551 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1552 $money_char. sprintf("%10.2f",$_->amount )
1557 my $balance_due_msg = $self->balance_due_msg;
1559 push @buf,['','-----------'];
1560 push @buf,[$balance_due_msg, $money_char.
1561 sprintf("%10.2f", $balance_due ) ];
1563 #create the template
1564 $template ||= $self->_agent_template;
1565 my $templatefile = 'invoice_template';
1566 $templatefile .= "_$template" if length($template);
1567 my @invoice_template = $conf->config($templatefile)
1568 or die "cannot load config file $templatefile";
1571 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1572 /invoice_lines\((\d*)\)/;
1573 $invoice_lines += $1 || scalar(@buf);
1576 die "no invoice_lines() functions in template?" unless $wasfunc;
1577 my $invoice_template = new Text::Template (
1579 SOURCE => [ map "$_\n", @invoice_template ],
1580 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1581 $invoice_template->compile()
1582 or die "can't compile template: $Text::Template::ERROR";
1584 #setup template variables
1585 package FS::cust_bill::_template; #!
1586 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1588 $invnum = $self->invnum;
1589 $date = $self->_date;
1591 $agent = $self->cust_main->agent->agent;
1593 if ( $FS::cust_bill::invoice_lines ) {
1595 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1597 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1602 #format address (variable for the template)
1604 @address = ( '', '', '', '', '', '' );
1605 package FS::cust_bill; #!
1606 $FS::cust_bill::_template::address[$l++] =
1607 $cust_main->payname.
1608 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1609 ? " (P.O. #". $cust_main->payinfo. ")"
1613 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1614 if $cust_main->company;
1615 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1616 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1617 if $cust_main->address2;
1618 $FS::cust_bill::_template::address[$l++] =
1619 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1621 my $countrydefault = $conf->config('countrydefault') || 'US';
1622 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1623 unless $cust_main->country eq $countrydefault;
1625 # #overdue? (variable for the template)
1626 # $FS::cust_bill::_template::overdue = (
1628 # && $today > $self->_date
1629 ## && $self->printed > 1
1630 # && $self->printed > 0
1633 #and subroutine for the template
1634 sub FS::cust_bill::_template::invoice_lines {
1635 my $lines = shift || scalar(@buf);
1637 scalar(@buf) ? shift @buf : [ '', '' ];
1643 $FS::cust_bill::_template::page = 1;
1647 push @collect, split("\n",
1648 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1650 $FS::cust_bill::_template::page++;
1653 map "$_\n", @collect;
1657 =item print_latex [ TIME [ , TEMPLATE ] ]
1659 Internal method - returns a filename of a filled-in LaTeX template for this
1660 invoice (Note: add ".tex" to get the actual filename).
1662 See print_ps and print_pdf for methods that return PostScript and PDF output.
1664 TIME an optional value used to control the printing of overdue messages. The
1665 default is now. It isn't the date of the invoice; that's the `_date' field.
1666 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1667 L<Time::Local> and L<Date::Parse> for conversion functions.
1671 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1674 my( $self, $today, $template ) = @_;
1676 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1679 my $cust_main = $self->cust_main;
1680 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1681 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1683 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1684 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1685 #my $balance_due = $self->owed + $pr_total - $cr_total;
1686 my $balance_due = $self->owed + $pr_total;
1688 #create the template
1689 $template ||= $self->_agent_template;
1690 my $templatefile = 'invoice_latex';
1691 my $suffix = length($template) ? "_$template" : '';
1692 $templatefile .= $suffix;
1693 my @invoice_template = map "$_\n", $conf->config($templatefile)
1694 or die "cannot load config file $templatefile";
1696 my($format, $text_template);
1697 if ( grep { /^%%Detail/ } @invoice_template ) {
1698 #change this to a die when the old code is removed
1699 warn "old-style invoice template $templatefile; ".
1700 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1703 $format = 'Text::Template';
1704 $text_template = new Text::Template(
1706 SOURCE => \@invoice_template,
1707 DELIMITERS => [ '[@--', '--@]' ],
1710 $text_template->compile()
1711 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1715 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1716 $returnaddress = join("\n",
1717 $conf->config_orbase('invoice_latexreturnaddress', $template)
1720 $returnaddress = '~';
1723 my %invoice_data = (
1724 'invnum' => $self->invnum,
1725 'date' => time2str('%b %o, %Y', $self->_date),
1726 'today' => time2str('%b %o, %Y', $today),
1727 'agent' => _latex_escape($cust_main->agent->agent),
1728 'payname' => _latex_escape($cust_main->payname),
1729 'company' => _latex_escape($cust_main->company),
1730 'address1' => _latex_escape($cust_main->address1),
1731 'address2' => _latex_escape($cust_main->address2),
1732 'city' => _latex_escape($cust_main->city),
1733 'state' => _latex_escape($cust_main->state),
1734 'zip' => _latex_escape($cust_main->zip),
1735 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1736 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1737 'returnaddress' => $returnaddress,
1739 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1740 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1741 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1744 my $countrydefault = $conf->config('countrydefault') || 'US';
1745 if ( $cust_main->country eq $countrydefault ) {
1746 $invoice_data{'country'} = '';
1748 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1751 $invoice_data{'notes'} =
1753 # #do variable substitutions in notes
1754 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1755 $conf->config_orbase('invoice_latexnotes', $template)
1757 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1760 $invoice_data{'footer'} =~ s/\n+$//;
1761 $invoice_data{'smallfooter'} =~ s/\n+$//;
1762 $invoice_data{'notes'} =~ s/\n+$//;
1764 $invoice_data{'po_line'} =
1765 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1766 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1770 if ( $format eq 'old' ) {
1773 my @total_item = ();
1774 while ( @invoice_template ) {
1775 my $line = shift @invoice_template;
1777 if ( $line =~ /^%%Detail\s*$/ ) {
1779 while ( ( my $line_item_line = shift @invoice_template )
1780 !~ /^%%EndDetail\s*$/ ) {
1781 push @line_item, $line_item_line;
1783 foreach my $line_item ( $self->_items ) {
1784 #foreach my $line_item ( $self->_items_pkg ) {
1785 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1786 $invoice_data{'description'} =
1787 _latex_escape($line_item->{'description'});
1788 if ( exists $line_item->{'ext_description'} ) {
1789 $invoice_data{'description'} .=
1790 "\\tabularnewline\n~~".
1791 join( "\\tabularnewline\n~~",
1792 map _latex_escape($_), @{$line_item->{'ext_description'}}
1795 $invoice_data{'amount'} = $line_item->{'amount'};
1796 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1798 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1801 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1803 while ( ( my $total_item_line = shift @invoice_template )
1804 !~ /^%%EndTotalDetails\s*$/ ) {
1805 push @total_item, $total_item_line;
1808 my @total_fill = ();
1811 foreach my $tax ( $self->_items_tax ) {
1812 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1813 $taxtotal += $tax->{'amount'};
1814 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1816 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1821 $invoice_data{'total_item'} = 'Sub-total';
1822 $invoice_data{'total_amount'} =
1823 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1824 unshift @total_fill,
1825 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1829 $invoice_data{'total_item'} = '\textbf{Total}';
1830 $invoice_data{'total_amount'} =
1831 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1833 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1836 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1839 foreach my $credit ( $self->_items_credits ) {
1840 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1842 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1844 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1849 foreach my $payment ( $self->_items_payments ) {
1850 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1852 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1854 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1858 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1859 $invoice_data{'total_amount'} =
1860 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1862 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1865 push @filled_in, @total_fill;
1868 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1869 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1870 push @filled_in, $line;
1881 } elsif ( $format eq 'Text::Template' ) {
1883 my @detail_items = ();
1884 my @total_items = ();
1886 $invoice_data{'detail_items'} = \@detail_items;
1887 $invoice_data{'total_items'} = \@total_items;
1889 foreach my $line_item ( $self->_items ) {
1891 ext_description => [],
1893 $detail->{'ref'} = $line_item->{'pkgnum'};
1894 $detail->{'quantity'} = 1;
1895 $detail->{'description'} = _latex_escape($line_item->{'description'});
1896 if ( exists $line_item->{'ext_description'} ) {
1897 @{$detail->{'ext_description'}} = map {
1899 } @{$line_item->{'ext_description'}};
1901 $detail->{'amount'} = $line_item->{'amount'};
1902 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1904 push @detail_items, $detail;
1909 foreach my $tax ( $self->_items_tax ) {
1911 $total->{'total_item'} = _latex_escape($tax->{'description'});
1912 $taxtotal += $tax->{'amount'};
1913 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1914 push @total_items, $total;
1919 $total->{'total_item'} = 'Sub-total';
1920 $total->{'total_amount'} =
1921 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1922 unshift @total_items, $total;
1927 $total->{'total_item'} = '\textbf{Total}';
1928 $total->{'total_amount'} =
1929 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1930 push @total_items, $total;
1933 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1936 foreach my $credit ( $self->_items_credits ) {
1938 $total->{'total_item'} = _latex_escape($credit->{'description'});
1940 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1941 push @total_items, $total;
1945 foreach my $payment ( $self->_items_payments ) {
1947 $total->{'total_item'} = _latex_escape($payment->{'description'});
1949 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1950 push @total_items, $total;
1955 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1956 $total->{'total_amount'} =
1957 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1958 push @total_items, $total;
1962 die "guru meditation #54";
1965 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1966 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1970 ) or die "can't open temp file: $!\n";
1971 if ( $format eq 'old' ) {
1972 print $fh join('', @filled_in );
1973 } elsif ( $format eq 'Text::Template' ) {
1974 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1976 die "guru meditation #32";
1980 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1985 =item print_ps [ TIME [ , TEMPLATE ] ]
1987 Returns an postscript invoice, as a scalar.
1989 TIME an optional value used to control the printing of overdue messages. The
1990 default is now. It isn't the date of the invoice; that's the `_date' field.
1991 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1992 L<Time::Local> and L<Date::Parse> for conversion functions.
1999 my $file = $self->print_latex(@_);
2001 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2004 my $sfile = shell_quote $file;
2006 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2007 or die "pslatex $file.tex failed; see $file.log for details?\n";
2008 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2009 or die "pslatex $file.tex failed; see $file.log for details?\n";
2011 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2012 or die "dvips failed";
2014 open(POSTSCRIPT, "<$file.ps")
2015 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2017 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2020 while (<POSTSCRIPT>) {
2030 =item print_pdf [ TIME [ , TEMPLATE ] ]
2032 Returns an PDF invoice, as a scalar.
2034 TIME an optional value used to control the printing of overdue messages. The
2035 default is now. It isn't the date of the invoice; that's the `_date' field.
2036 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2037 L<Time::Local> and L<Date::Parse> for conversion functions.
2044 my $file = $self->print_latex(@_);
2046 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2049 #system('pdflatex', "$file.tex");
2050 #system('pdflatex', "$file.tex");
2051 #! LaTeX Error: Unknown graphics extension: .eps.
2053 my $sfile = shell_quote $file;
2055 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2056 or die "pslatex $file.tex failed; see $file.log for details?\n";
2057 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2058 or die "pslatex $file.tex failed; see $file.log for details?\n";
2060 #system('dvipdf', "$file.dvi", "$file.pdf" );
2062 "dvips -q -t letter -f $sfile.dvi ".
2063 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2066 or die "dvips | gs failed: $!";
2068 open(PDF, "<$file.pdf")
2069 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2071 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2084 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2086 Returns an HTML invoice, as a scalar.
2088 TIME an optional value used to control the printing of overdue messages. The
2089 default is now. It isn't the date of the invoice; that's the `_date' field.
2090 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2091 L<Time::Local> and L<Date::Parse> for conversion functions.
2093 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2094 when emailing the invoice as part of a multipart/related MIME email.
2098 #some falze laziness w/print_text and print_latex (and send_csv)
2100 my( $self, $today, $template, $cid ) = @_;
2103 my $cust_main = $self->cust_main;
2104 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2105 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2107 $template ||= $self->_agent_template;
2108 my $templatefile = 'invoice_html';
2109 my $suffix = length($template) ? "_$template" : '';
2110 $templatefile .= $suffix;
2111 my @html_template = map "$_\n", $conf->config($templatefile)
2112 or die "cannot load config file $templatefile";
2114 my $html_template = new Text::Template(
2116 SOURCE => \@html_template,
2117 DELIMITERS => [ '<%=', '%>' ],
2120 $html_template->compile()
2121 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2123 my %invoice_data = (
2124 'invnum' => $self->invnum,
2125 'date' => time2str('%b %o, %Y', $self->_date),
2126 'today' => time2str('%b %o, %Y', $today),
2127 'agent' => encode_entities($cust_main->agent->agent),
2128 'payname' => encode_entities($cust_main->payname),
2129 'company' => encode_entities($cust_main->company),
2130 'address1' => encode_entities($cust_main->address1),
2131 'address2' => encode_entities($cust_main->address2),
2132 'city' => encode_entities($cust_main->city),
2133 'state' => encode_entities($cust_main->state),
2134 'zip' => encode_entities($cust_main->zip),
2135 'terms' => $conf->config('invoice_default_terms')
2136 || 'Payable upon receipt',
2138 'template' => $template,
2139 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2143 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2144 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2146 $invoice_data{'returnaddress'} =
2147 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2149 $invoice_data{'returnaddress'} =
2152 s/\\\\\*?\s*$/<BR>/;
2153 s/\\hyphenation\{[\w\s\-]+\}//;
2156 $conf->config_orbase( 'invoice_latexreturnaddress',
2162 my $countrydefault = $conf->config('countrydefault') || 'US';
2163 if ( $cust_main->country eq $countrydefault ) {
2164 $invoice_data{'country'} = '';
2166 $invoice_data{'country'} =
2167 encode_entities(code2country($cust_main->country));
2171 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2172 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2174 $invoice_data{'notes'} =
2175 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2177 $invoice_data{'notes'} =
2179 s/%%(.*)$/<!-- $1 -->/;
2180 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2181 s/\\begin\{enumerate\}/<ol>/;
2183 s/\\end\{enumerate\}/<\/ol>/;
2184 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2187 $conf->config_orbase('invoice_latexnotes', $template)
2191 # #do variable substitutions in notes
2192 # $invoice_data{'notes'} =
2194 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2195 # $conf->config_orbase('invoice_latexnotes', $suffix)
2199 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2200 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2202 $invoice_data{'footer'} =
2203 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2205 $invoice_data{'footer'} =
2206 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2207 $conf->config_orbase('invoice_latexfooter', $template)
2211 $invoice_data{'po_line'} =
2212 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2213 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2216 my $money_char = $conf->config('money_char') || '$';
2218 foreach my $line_item ( $self->_items ) {
2220 ext_description => [],
2222 $detail->{'ref'} = $line_item->{'pkgnum'};
2223 $detail->{'description'} = encode_entities($line_item->{'description'});
2224 if ( exists $line_item->{'ext_description'} ) {
2225 @{$detail->{'ext_description'}} = map {
2226 encode_entities($_);
2227 } @{$line_item->{'ext_description'}};
2229 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2230 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2232 push @{$invoice_data{'detail_items'}}, $detail;
2237 foreach my $tax ( $self->_items_tax ) {
2239 $total->{'total_item'} = encode_entities($tax->{'description'});
2240 $taxtotal += $tax->{'amount'};
2241 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2242 push @{$invoice_data{'total_items'}}, $total;
2247 $total->{'total_item'} = 'Sub-total';
2248 $total->{'total_amount'} =
2249 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2250 unshift @{$invoice_data{'total_items'}}, $total;
2253 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2256 $total->{'total_item'} = '<b>Total</b>';
2257 $total->{'total_amount'} =
2258 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2259 push @{$invoice_data{'total_items'}}, $total;
2262 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2265 foreach my $credit ( $self->_items_credits ) {
2267 $total->{'total_item'} = encode_entities($credit->{'description'});
2269 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2270 push @{$invoice_data{'total_items'}}, $total;
2274 foreach my $payment ( $self->_items_payments ) {
2276 $total->{'total_item'} = encode_entities($payment->{'description'});
2278 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2279 push @{$invoice_data{'total_items'}}, $total;
2284 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2285 $total->{'total_amount'} =
2286 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2287 push @{$invoice_data{'total_items'}}, $total;
2290 $html_template->fill_in( HASH => \%invoice_data);
2293 # quick subroutine for print_latex
2295 # There are ten characters that LaTeX treats as special characters, which
2296 # means that they do not simply typeset themselves:
2297 # # $ % & ~ _ ^ \ { }
2299 # TeX ignores blanks following an escaped character; if you want a blank (as
2300 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2304 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2305 $value =~ s/([<>])/\$$1\$/g;
2309 #utility methods for print_*
2311 sub balance_due_msg {
2313 my $msg = 'Balance Due';
2314 return $msg unless $conf->exists('invoice_default_terms');
2315 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2316 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2317 } elsif ( $conf->config('invoice_default_terms') ) {
2318 $msg .= ' - '. $conf->config('invoice_default_terms');
2325 my @display = scalar(@_)
2327 : qw( _items_previous _items_pkg );
2328 #: qw( _items_pkg );
2329 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2331 foreach my $display ( @display ) {
2332 push @b, $self->$display(@_);
2337 sub _items_previous {
2339 my $cust_main = $self->cust_main;
2340 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2342 foreach ( @pr_cust_bill ) {
2344 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2345 ' ('. time2str('%x',$_->_date). ')',
2346 #'pkgpart' => 'N/A',
2348 'amount' => sprintf("%.2f", $_->owed),
2354 # 'description' => 'Previous Balance',
2355 # #'pkgpart' => 'N/A',
2356 # 'pkgnum' => 'N/A',
2357 # 'amount' => sprintf("%10.2f", $pr_total ),
2358 # 'ext_description' => [ map {
2359 # "Invoice ". $_->invnum.
2360 # " (". time2str("%x",$_->_date). ") ".
2361 # sprintf("%10.2f", $_->owed)
2362 # } @pr_cust_bill ],
2369 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2370 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2375 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2376 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2379 sub _items_cust_bill_pkg {
2381 my $cust_bill_pkg = shift;
2384 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2386 my $desc = $cust_bill_pkg->desc;
2388 if ( $cust_bill_pkg->pkgnum > 0 ) {
2390 if ( $cust_bill_pkg->setup != 0 ) {
2391 my $description = $desc;
2392 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2393 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2394 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2396 description => $description,
2397 #pkgpart => $part_pkg->pkgpart,
2398 pkgnum => $cust_bill_pkg->pkgnum,
2399 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2400 ext_description => \@d,
2404 if ( $cust_bill_pkg->recur != 0 ) {
2406 description => "$desc (" .
2407 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2408 time2str('%x', $cust_bill_pkg->edate). ')',
2409 #pkgpart => $part_pkg->pkgpart,
2410 pkgnum => $cust_bill_pkg->pkgnum,
2411 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2413 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2414 $cust_bill_pkg->sdate),
2415 $cust_bill_pkg->details,
2420 } else { #pkgnum tax or one-shot line item (??)
2422 if ( $cust_bill_pkg->setup != 0 ) {
2424 'description' => $desc,
2425 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2428 if ( $cust_bill_pkg->recur != 0 ) {
2430 'description' => "$desc (".
2431 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2432 time2str("%x", $cust_bill_pkg->edate). ')',
2433 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2445 sub _items_credits {
2450 foreach ( $self->cust_credited ) {
2452 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2454 my $reason = $_->cust_credit->reason;
2455 #my $reason = substr($_->cust_credit->reason,0,32);
2456 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2457 $reason = " ($reason) " if $reason;
2459 #'description' => 'Credit ref\#'. $_->crednum.
2460 # " (". time2str("%x",$_->cust_credit->_date) .")".
2462 'description' => 'Credit applied '.
2463 time2str("%x",$_->cust_credit->_date). $reason,
2464 'amount' => sprintf("%.2f",$_->amount),
2467 #foreach ( @cr_cust_credit ) {
2469 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2470 # $money_char. sprintf("%10.2f",$_->credited)
2478 sub _items_payments {
2482 #get & print payments
2483 foreach ( $self->cust_bill_pay ) {
2485 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2488 'description' => "Payment received ".
2489 time2str("%x",$_->cust_pay->_date ),
2490 'amount' => sprintf("%.2f", $_->amount )
2509 sub process_reprint {
2510 process_re_X('print', @_);
2517 sub process_reemail {
2518 process_re_X('email', @_);
2526 process_re_X('fax', @_);
2529 use Storable qw(thaw);
2533 my( $method, $job ) = ( shift, shift );
2534 warn "process_re_X $method for job $job\n" if $DEBUG;
2536 my $param = thaw(decode_base64(shift));
2537 warn Dumper($param) if $DEBUG;
2548 my($method, $job, %param ) = @_;
2549 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2551 warn "re_X $method for job $job with param:\n".
2552 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2555 #some false laziness w/search/cust_bill.html
2557 my $orderby = 'ORDER BY cust_bill._date';
2561 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2562 push @where, "cust_bill._date >= $1";
2564 if ( $param{'end'} =~ /^(\d+)$/ ) {
2565 push @where, "cust_bill._date < $1";
2567 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2568 push @where, "cust_main.agentnum = $1";
2572 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2573 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2574 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2575 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2577 push @where, "0 != $owed"
2580 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2583 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2585 my $addl_from = 'left join cust_main using ( custnum )';
2587 if ( $param{'newest_percust'} ) {
2588 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2589 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2590 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2593 my @cust_bill = qsearch( 'cust_bill',
2595 "$distinct cust_bill.*",
2601 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2602 foreach my $cust_bill ( @cust_bill ) {
2603 $cust_bill->$method();
2605 if ( $job ) { #progressbar foo
2607 if ( time - $min_sec > $last ) {
2608 my $error = $job->update_statustext(
2609 int( 100 * $num / scalar(@cust_bill) )
2611 die $error if $error;
2626 print_text formatting (and some logic :/) is in source, but needs to be
2627 slurped in from a file. Also number of lines ($=).
2631 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2632 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base