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;
1187 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1189 my( $previous_balance, @unused ) = $self->previous; #previous balance
1191 my $pmt_cr_applied = 0;
1192 $pmt_cr_applied += $_->{'amount'}
1193 foreach ( $self->_items_payments, $self->_items_credits ) ;
1195 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1198 '', # 1 | N/A-Leave Empty CHAR 2
1199 '', # 2 | N/A-Leave Empty CHAR 15
1200 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1201 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1202 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1203 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1204 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1205 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1206 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1207 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1208 '', # 10 | Ancillary Billing Information CHAR 30
1209 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1210 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1213 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1216 $duedate, # 14 | Bill Due Date CHAR 10
1218 $previous_balance, # 15 | Previous Balance NUM* 9
1219 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1220 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1221 $totaldue, # 18 | Total Amt Due NUM* 9
1222 $totaldue, # 19 | Total Amt Due NUM* 9
1223 '', # 20 | 30 Day Aging NUM* 9
1224 '', # 21 | 60 Day Aging NUM* 9
1225 '', # 22 | 90 Day Aging NUM* 9
1226 'N', # 23 | Y/N CHAR 1
1227 '', # 24 | Remittance automation CHAR 100
1228 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1229 $self->custnum, # 26 | Customer Reference Number CHAR 15
1230 '0', # 27 | Federal Tax*** NUM* 9
1231 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1232 '0', # 29 | Other Taxes & Fees*** NUM* 9
1241 time2str("%x", $self->_date),
1242 sprintf("%.2f", $self->charged),
1243 ( map { $cust_main->getfield($_) }
1244 qw( first last company address1 address2 city state zip country ) ),
1246 ) or die "can't create csv";
1249 my $header = $csv->string. "\n";
1252 if ( lc($opt{'format'}) eq 'billco' ) {
1255 foreach my $item ( $self->_items_pkg ) {
1258 '', # 1 | N/A-Leave Empty CHAR 2
1259 '', # 2 | N/A-Leave Empty CHAR 15
1260 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1261 $self->invnum, # 4 | Invoice Number CHAR 15
1262 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1263 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1264 $item->{'amount'}, # 7 | Amount NUM* 9
1265 '', # 8 | Line Format Control** CHAR 2
1266 '', # 9 | Grouping Code CHAR 2
1267 '', # 10 | User Defined CHAR 15
1270 $detail .= $csv->string. "\n";
1276 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1278 my($pkg, $setup, $recur, $sdate, $edate);
1279 if ( $cust_bill_pkg->pkgnum ) {
1281 ($pkg, $setup, $recur, $sdate, $edate) = (
1282 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1283 ( $cust_bill_pkg->setup != 0
1284 ? sprintf("%.2f", $cust_bill_pkg->setup )
1286 ( $cust_bill_pkg->recur != 0
1287 ? sprintf("%.2f", $cust_bill_pkg->recur )
1289 ( $cust_bill_pkg->sdate
1290 ? time2str("%x", $cust_bill_pkg->sdate)
1292 ($cust_bill_pkg->edate
1293 ?time2str("%x", $cust_bill_pkg->edate)
1297 } else { #pkgnum tax
1298 next unless $cust_bill_pkg->setup != 0;
1299 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1300 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1302 ($pkg, $setup, $recur, $sdate, $edate) =
1303 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1309 ( map { '' } (1..11) ),
1310 ($pkg, $setup, $recur, $sdate, $edate)
1311 ) or die "can't create csv";
1313 $detail .= $csv->string. "\n";
1319 ( $header, $detail );
1325 Pays this invoice with a compliemntary payment. If there is an error,
1326 returns the error, otherwise returns false.
1332 my $cust_pay = new FS::cust_pay ( {
1333 'invnum' => $self->invnum,
1334 'paid' => $self->owed,
1337 'payinfo' => $self->cust_main->payinfo,
1345 Attempts to pay this invoice with a credit card payment via a
1346 Business::OnlinePayment realtime gateway. See
1347 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1348 for supported processors.
1354 $self->realtime_bop( 'CC', @_ );
1359 Attempts to pay this invoice with an electronic check (ACH) payment via a
1360 Business::OnlinePayment realtime gateway. See
1361 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1362 for supported processors.
1368 $self->realtime_bop( 'ECHECK', @_ );
1373 Attempts to pay this invoice with phone bill (LEC) payment via a
1374 Business::OnlinePayment realtime gateway. See
1375 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1376 for supported processors.
1382 $self->realtime_bop( 'LEC', @_ );
1386 my( $self, $method ) = @_;
1388 my $cust_main = $self->cust_main;
1389 my $balance = $cust_main->balance;
1390 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1391 $amount = sprintf("%.2f", $amount);
1392 return "not run (balance $balance)" unless $amount > 0;
1394 my $description = 'Internet Services';
1395 if ( $conf->exists('business-onlinepayment-description') ) {
1396 my $dtempl = $conf->config('business-onlinepayment-description');
1398 my $agent_obj = $cust_main->agent
1399 or die "can't retreive agent for $cust_main (agentnum ".
1400 $cust_main->agentnum. ")";
1401 my $agent = $agent_obj->agent;
1402 my $pkgs = join(', ',
1403 map { $_->cust_pkg->part_pkg->pkg }
1404 grep { $_->pkgnum } $self->cust_bill_pkg
1406 $description = eval qq("$dtempl");
1409 $cust_main->realtime_bop($method, $amount,
1410 'description' => $description,
1411 'invnum' => $self->invnum,
1416 =item batch_card OPTION => VALUE...
1418 Adds a payment for this invoice to the pending credit card batch (see
1419 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1420 runs the payment using a realtime gateway.
1425 my ($self, %options) = @_;
1426 my $cust_main = $self->cust_main;
1428 $options{invnum} = $self->invnum;
1430 $cust_main->batch_card(%options);
1433 sub _agent_template {
1435 $self->cust_main->agent_template;
1438 sub _agent_invoice_from {
1440 $self->cust_main->agent_invoice_from;
1443 =item print_text [ TIME [ , TEMPLATE ] ]
1445 Returns an text invoice, as a list of lines.
1447 TIME an optional value used to control the printing of overdue messages. The
1448 default is now. It isn't the date of the invoice; that's the `_date' field.
1449 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1450 L<Time::Local> and L<Date::Parse> for conversion functions.
1454 #still some false laziness w/_items stuff (and send_csv)
1457 my( $self, $today, $template ) = @_;
1460 # my $invnum = $self->invnum;
1461 my $cust_main = $self->cust_main;
1462 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1463 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1465 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1466 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1467 #my $balance_due = $self->owed + $pr_total - $cr_total;
1468 my $balance_due = $self->owed + $pr_total;
1471 #my($description,$amount);
1475 foreach ( @pr_cust_bill ) {
1477 "Previous Balance, Invoice #". $_->invnum.
1478 " (". time2str("%x",$_->_date). ")",
1479 $money_char. sprintf("%10.2f",$_->owed)
1482 if (@pr_cust_bill) {
1483 push @buf,['','-----------'];
1484 push @buf,[ 'Total Previous Balance',
1485 $money_char. sprintf("%10.2f",$pr_total ) ];
1490 foreach my $cust_bill_pkg (
1491 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1492 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1495 my $desc = $cust_bill_pkg->desc;
1497 if ( $cust_bill_pkg->pkgnum > 0 ) {
1499 if ( $cust_bill_pkg->setup != 0 ) {
1500 my $description = $desc;
1501 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1502 push @buf, [ $description,
1503 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1505 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1506 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1509 if ( $cust_bill_pkg->recur != 0 ) {
1512 ( $conf->exists('disable_line_item_date_ranges')
1514 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1515 time2str("%x", $cust_bill_pkg->edate) . ")"
1517 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1520 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1521 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1522 $cust_bill_pkg->sdate );
1525 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1527 } else { #pkgnum tax or one-shot line item
1529 if ( $cust_bill_pkg->setup != 0 ) {
1531 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1533 if ( $cust_bill_pkg->recur != 0 ) {
1534 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1535 . time2str("%x", $cust_bill_pkg->edate). ")",
1536 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1544 push @buf,['','-----------'];
1545 push @buf,['Total New Charges',
1546 $money_char. sprintf("%10.2f",$self->charged) ];
1549 push @buf,['','-----------'];
1550 push @buf,['Total Charges',
1551 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1555 foreach ( $self->cust_credited ) {
1557 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1559 my $reason = substr($_->cust_credit->reason,0,32);
1560 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1561 $reason = " ($reason) " if $reason;
1563 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1565 $money_char. sprintf("%10.2f",$_->amount)
1568 #foreach ( @cr_cust_credit ) {
1570 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1571 # $money_char. sprintf("%10.2f",$_->credited)
1575 #get & print payments
1576 foreach ( $self->cust_bill_pay ) {
1578 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1581 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1582 $money_char. sprintf("%10.2f",$_->amount )
1587 my $balance_due_msg = $self->balance_due_msg;
1589 push @buf,['','-----------'];
1590 push @buf,[$balance_due_msg, $money_char.
1591 sprintf("%10.2f", $balance_due ) ];
1593 #create the template
1594 $template ||= $self->_agent_template;
1595 my $templatefile = 'invoice_template';
1596 $templatefile .= "_$template" if length($template);
1597 my @invoice_template = $conf->config($templatefile)
1598 or die "cannot load config file $templatefile";
1601 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1602 /invoice_lines\((\d*)\)/;
1603 $invoice_lines += $1 || scalar(@buf);
1606 die "no invoice_lines() functions in template?" unless $wasfunc;
1607 my $invoice_template = new Text::Template (
1609 SOURCE => [ map "$_\n", @invoice_template ],
1610 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1611 $invoice_template->compile()
1612 or die "can't compile template: $Text::Template::ERROR";
1614 #setup template variables
1615 package FS::cust_bill::_template; #!
1616 use vars qw( $custnum $invnum $date $agent @address $overdue
1617 $page $total_pages @buf );
1619 $custnum = $self->custnum;
1620 $invnum = $self->invnum;
1621 $date = $self->_date;
1622 $agent = $self->cust_main->agent->agent;
1625 if ( $FS::cust_bill::invoice_lines ) {
1627 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1629 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1634 #format address (variable for the template)
1636 @address = ( '', '', '', '', '', '' );
1637 package FS::cust_bill; #!
1638 $FS::cust_bill::_template::address[$l++] =
1639 $cust_main->payname.
1640 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1641 ? " (P.O. #". $cust_main->payinfo. ")"
1645 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1646 if $cust_main->company;
1647 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1648 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1649 if $cust_main->address2;
1650 $FS::cust_bill::_template::address[$l++] =
1651 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1653 my $countrydefault = $conf->config('countrydefault') || 'US';
1654 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1655 unless $cust_main->country eq $countrydefault;
1657 # #overdue? (variable for the template)
1658 # $FS::cust_bill::_template::overdue = (
1660 # && $today > $self->_date
1661 ## && $self->printed > 1
1662 # && $self->printed > 0
1665 #and subroutine for the template
1666 sub FS::cust_bill::_template::invoice_lines {
1667 my $lines = shift || scalar(@buf);
1669 scalar(@buf) ? shift @buf : [ '', '' ];
1675 $FS::cust_bill::_template::page = 1;
1679 push @collect, split("\n",
1680 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1682 $FS::cust_bill::_template::page++;
1685 map "$_\n", @collect;
1689 =item print_latex [ TIME [ , TEMPLATE ] ]
1691 Internal method - returns a filename of a filled-in LaTeX template for this
1692 invoice (Note: add ".tex" to get the actual filename), and a filename of
1693 an associated logo (with the .eps extension included).
1695 See print_ps and print_pdf for methods that return PostScript and PDF output.
1697 TIME an optional value used to control the printing of overdue messages. The
1698 default is now. It isn't the date of the invoice; that's the `_date' field.
1699 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1700 L<Time::Local> and L<Date::Parse> for conversion functions.
1704 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1707 my( $self, $today, $template ) = @_;
1709 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1712 my $cust_main = $self->cust_main;
1713 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1714 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1716 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1717 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1718 #my $balance_due = $self->owed + $pr_total - $cr_total;
1719 my $balance_due = $self->owed + $pr_total;
1721 #create the template
1722 $template ||= $self->_agent_template;
1723 my $templatefile = 'invoice_latex';
1724 my $suffix = length($template) ? "_$template" : '';
1725 $templatefile .= $suffix;
1726 my @invoice_template = map "$_\n", $conf->config($templatefile)
1727 or die "cannot load config file $templatefile";
1729 my($format, $text_template);
1730 if ( grep { /^%%Detail/ } @invoice_template ) {
1731 #change this to a die when the old code is removed
1732 warn "old-style invoice template $templatefile; ".
1733 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1736 $format = 'Text::Template';
1737 $text_template = new Text::Template(
1739 SOURCE => \@invoice_template,
1740 DELIMITERS => [ '[@--', '--@]' ],
1743 $text_template->compile()
1744 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1748 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1749 $returnaddress = join("\n",
1750 $conf->config_orbase('invoice_latexreturnaddress', $template)
1753 $returnaddress = '~';
1756 my %invoice_data = (
1757 'custnum' => $self->custnum,
1758 'invnum' => $self->invnum,
1759 'date' => time2str('%b %o, %Y', $self->_date),
1760 'today' => time2str('%b %o, %Y', $today),
1761 'agent' => _latex_escape($cust_main->agent->agent),
1762 'payname' => _latex_escape($cust_main->payname),
1763 'company' => _latex_escape($cust_main->company),
1764 'address1' => _latex_escape($cust_main->address1),
1765 'address2' => _latex_escape($cust_main->address2),
1766 'city' => _latex_escape($cust_main->city),
1767 'state' => _latex_escape($cust_main->state),
1768 'zip' => _latex_escape($cust_main->zip),
1769 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1770 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1771 'returnaddress' => $returnaddress,
1773 'terms' => $self->terms,
1774 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1775 # better hang on to conf_dir for a while
1776 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1779 my $countrydefault = $conf->config('countrydefault') || 'US';
1780 if ( $cust_main->country eq $countrydefault ) {
1781 $invoice_data{'country'} = '';
1783 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1786 $invoice_data{'notes'} =
1788 # #do variable substitutions in notes
1789 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1790 $conf->config_orbase('invoice_latexnotes', $template)
1792 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1795 $invoice_data{'footer'} =~ s/\n+$//;
1796 $invoice_data{'smallfooter'} =~ s/\n+$//;
1797 $invoice_data{'notes'} =~ s/\n+$//;
1799 $invoice_data{'po_line'} =
1800 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1801 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1805 if ( $format eq 'old' ) {
1808 my @total_item = ();
1809 while ( @invoice_template ) {
1810 my $line = shift @invoice_template;
1812 if ( $line =~ /^%%Detail\s*$/ ) {
1814 while ( ( my $line_item_line = shift @invoice_template )
1815 !~ /^%%EndDetail\s*$/ ) {
1816 push @line_item, $line_item_line;
1818 foreach my $line_item ( $self->_items ) {
1819 #foreach my $line_item ( $self->_items_pkg ) {
1820 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1821 $invoice_data{'description'} =
1822 _latex_escape($line_item->{'description'});
1823 if ( exists $line_item->{'ext_description'} ) {
1824 $invoice_data{'description'} .=
1825 "\\tabularnewline\n~~".
1826 join( "\\tabularnewline\n~~",
1827 map _latex_escape($_), @{$line_item->{'ext_description'}}
1830 $invoice_data{'amount'} = $line_item->{'amount'};
1831 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1833 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1836 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1838 while ( ( my $total_item_line = shift @invoice_template )
1839 !~ /^%%EndTotalDetails\s*$/ ) {
1840 push @total_item, $total_item_line;
1843 my @total_fill = ();
1846 foreach my $tax ( $self->_items_tax ) {
1847 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1848 $taxtotal += $tax->{'amount'};
1849 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1851 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1856 $invoice_data{'total_item'} = 'Sub-total';
1857 $invoice_data{'total_amount'} =
1858 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1859 unshift @total_fill,
1860 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1864 $invoice_data{'total_item'} = '\textbf{Total}';
1865 $invoice_data{'total_amount'} =
1866 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1868 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1871 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1874 foreach my $credit ( $self->_items_credits ) {
1875 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1877 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1879 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1884 foreach my $payment ( $self->_items_payments ) {
1885 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1887 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1889 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1893 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1894 $invoice_data{'total_amount'} =
1895 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1897 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1900 push @filled_in, @total_fill;
1903 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1904 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1905 push @filled_in, $line;
1916 } elsif ( $format eq 'Text::Template' ) {
1918 my @detail_items = ();
1919 my @total_items = ();
1921 $invoice_data{'detail_items'} = \@detail_items;
1922 $invoice_data{'total_items'} = \@total_items;
1924 foreach my $line_item ( $self->_items ) {
1926 ext_description => [],
1928 $detail->{'ref'} = $line_item->{'pkgnum'};
1929 $detail->{'quantity'} = 1;
1930 $detail->{'description'} = _latex_escape($line_item->{'description'});
1931 if ( exists $line_item->{'ext_description'} ) {
1932 @{$detail->{'ext_description'}} = map {
1934 } @{$line_item->{'ext_description'}};
1936 $detail->{'amount'} = $line_item->{'amount'};
1937 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1939 push @detail_items, $detail;
1944 foreach my $tax ( $self->_items_tax ) {
1946 $total->{'total_item'} = _latex_escape($tax->{'description'});
1947 $taxtotal += $tax->{'amount'};
1948 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1949 push @total_items, $total;
1954 $total->{'total_item'} = 'Sub-total';
1955 $total->{'total_amount'} =
1956 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1957 unshift @total_items, $total;
1962 $total->{'total_item'} = '\textbf{Total}';
1963 $total->{'total_amount'} =
1964 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1965 push @total_items, $total;
1968 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1971 foreach my $credit ( $self->_items_credits ) {
1973 $total->{'total_item'} = _latex_escape($credit->{'description'});
1975 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1976 push @total_items, $total;
1980 foreach my $payment ( $self->_items_payments ) {
1982 $total->{'total_item'} = _latex_escape($payment->{'description'});
1984 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1985 push @total_items, $total;
1990 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1991 $total->{'total_amount'} =
1992 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1993 push @total_items, $total;
1997 die "guru meditation #54";
2000 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2001 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2005 ) or die "can't open temp file: $!\n";
2007 if ($template && $conf->exists("logo_${template}.eps")) {
2008 print $lh $conf->config_binary("logo_${template}.eps")
2009 or die "can't write temp file: $!\n";
2011 print $lh $conf->config_binary('logo.eps')
2012 or die "can't write temp file: $!\n";
2015 $invoice_data{'logo_file'} = $lh->filename;
2017 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2021 ) or die "can't open temp file: $!\n";
2022 if ( $format eq 'old' ) {
2023 print $fh join('', @filled_in );
2024 } elsif ( $format eq 'Text::Template' ) {
2025 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2027 die "guru meditation #32";
2031 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2032 return ($1, $invoice_data{'logo_file'});
2036 =item print_ps [ TIME [ , TEMPLATE ] ]
2038 Returns an postscript invoice, as a scalar.
2040 TIME an optional value used to control the printing of overdue messages. The
2041 default is now. It isn't the date of the invoice; that's the `_date' field.
2042 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2043 L<Time::Local> and L<Date::Parse> for conversion functions.
2050 my ($file, $lfile) = $self->print_latex(@_);
2051 my $ps = generate_ps($file);
2057 =item print_pdf [ TIME [ , TEMPLATE ] ]
2059 Returns an PDF invoice, as a scalar.
2061 TIME an optional value used to control the printing of overdue messages. The
2062 default is now. It isn't the date of the invoice; that's the `_date' field.
2063 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2064 L<Time::Local> and L<Date::Parse> for conversion functions.
2071 my ($file, $lfile) = $self->print_latex(@_);
2073 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2076 #system('pdflatex', "$file.tex");
2077 #system('pdflatex', "$file.tex");
2078 #! LaTeX Error: Unknown graphics extension: .eps.
2080 my $sfile = shell_quote $file;
2082 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2083 or die "pslatex $file.tex failed; see $file.log for details?\n";
2084 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2085 or die "pslatex $file.tex failed; see $file.log for details?\n";
2087 #system('dvipdf', "$file.dvi", "$file.pdf" );
2089 "dvips -q -t letter -f $sfile.dvi ".
2090 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2093 or die "dvips | gs failed: $!";
2095 open(PDF, "<$file.pdf")
2096 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2098 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2112 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2114 Returns an HTML invoice, as a scalar.
2116 TIME an optional value used to control the printing of overdue messages. The
2117 default is now. It isn't the date of the invoice; that's the `_date' field.
2118 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2119 L<Time::Local> and L<Date::Parse> for conversion functions.
2121 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2122 when emailing the invoice as part of a multipart/related MIME email.
2126 #some falze laziness w/print_text and print_latex (and send_csv)
2128 my( $self, $today, $template, $cid ) = @_;
2131 my $cust_main = $self->cust_main;
2132 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2133 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2135 $template ||= $self->_agent_template;
2136 my $templatefile = 'invoice_html';
2137 my $suffix = length($template) ? "_$template" : '';
2138 $templatefile .= $suffix;
2139 my @html_template = map "$_\n", $conf->config($templatefile)
2140 or die "cannot load config file $templatefile";
2142 my $html_template = new Text::Template(
2144 SOURCE => \@html_template,
2145 DELIMITERS => [ '<%=', '%>' ],
2148 $html_template->compile()
2149 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2151 my %invoice_data = (
2152 'custnum' => $self->custnum,
2153 'invnum' => $self->invnum,
2154 'date' => time2str('%b %o, %Y', $self->_date),
2155 'today' => time2str('%b %o, %Y', $today),
2156 'agent' => encode_entities($cust_main->agent->agent),
2157 'payname' => encode_entities($cust_main->payname),
2158 'company' => encode_entities($cust_main->company),
2159 'address1' => encode_entities($cust_main->address1),
2160 'address2' => encode_entities($cust_main->address2),
2161 'city' => encode_entities($cust_main->city),
2162 'state' => encode_entities($cust_main->state),
2163 'zip' => encode_entities($cust_main->zip),
2164 'terms' => $self->terms,
2166 'template' => $template,
2167 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2171 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2172 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2174 $invoice_data{'returnaddress'} =
2175 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2177 $invoice_data{'returnaddress'} =
2180 s/\\\\\*?\s*$/<BR>/;
2181 s/\\hyphenation\{[\w\s\-]+\}//;
2184 $conf->config_orbase( 'invoice_latexreturnaddress',
2190 my $countrydefault = $conf->config('countrydefault') || 'US';
2191 if ( $cust_main->country eq $countrydefault ) {
2192 $invoice_data{'country'} = '';
2194 $invoice_data{'country'} =
2195 encode_entities(code2country($cust_main->country));
2199 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2200 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2202 $invoice_data{'notes'} =
2203 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2205 $invoice_data{'notes'} =
2207 s/%%(.*)$/<!-- $1 -->/;
2208 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2209 s/\\begin\{enumerate\}/<ol>/;
2211 s/\\end\{enumerate\}/<\/ol>/;
2212 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2216 $conf->config_orbase('invoice_latexnotes', $template)
2220 # #do variable substitutions in notes
2221 # $invoice_data{'notes'} =
2223 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2224 # $conf->config_orbase('invoice_latexnotes', $suffix)
2228 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2229 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2231 $invoice_data{'footer'} =
2232 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2234 $invoice_data{'footer'} =
2235 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2236 $conf->config_orbase('invoice_latexfooter', $template)
2240 $invoice_data{'po_line'} =
2241 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2242 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2245 my $money_char = $conf->config('money_char') || '$';
2247 foreach my $line_item ( $self->_items ) {
2249 ext_description => [],
2251 $detail->{'ref'} = $line_item->{'pkgnum'};
2252 $detail->{'description'} = encode_entities($line_item->{'description'});
2253 if ( exists $line_item->{'ext_description'} ) {
2254 @{$detail->{'ext_description'}} = map {
2255 encode_entities($_);
2256 } @{$line_item->{'ext_description'}};
2258 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2259 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2261 push @{$invoice_data{'detail_items'}}, $detail;
2266 foreach my $tax ( $self->_items_tax ) {
2268 $total->{'total_item'} = encode_entities($tax->{'description'});
2269 $taxtotal += $tax->{'amount'};
2270 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2271 push @{$invoice_data{'total_items'}}, $total;
2276 $total->{'total_item'} = 'Sub-total';
2277 $total->{'total_amount'} =
2278 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2279 unshift @{$invoice_data{'total_items'}}, $total;
2282 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2285 $total->{'total_item'} = '<b>Total</b>';
2286 $total->{'total_amount'} =
2287 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2288 push @{$invoice_data{'total_items'}}, $total;
2291 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2294 foreach my $credit ( $self->_items_credits ) {
2296 $total->{'total_item'} = encode_entities($credit->{'description'});
2298 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2299 push @{$invoice_data{'total_items'}}, $total;
2303 foreach my $payment ( $self->_items_payments ) {
2305 $total->{'total_item'} = encode_entities($payment->{'description'});
2307 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2308 push @{$invoice_data{'total_items'}}, $total;
2313 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2314 $total->{'total_amount'} =
2315 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2316 push @{$invoice_data{'total_items'}}, $total;
2319 $html_template->fill_in( HASH => \%invoice_data);
2322 # quick subroutine for print_latex
2324 # There are ten characters that LaTeX treats as special characters, which
2325 # means that they do not simply typeset themselves:
2326 # # $ % & ~ _ ^ \ { }
2328 # TeX ignores blanks following an escaped character; if you want a blank (as
2329 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2333 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2334 $value =~ s/([<>])/\$$1\$/g;
2338 #utility methods for print_*
2343 #check for an invoice- specific override (eventually)
2345 #check for a customer- specific override
2346 return $self->cust_main->invoice_terms
2347 if $self->cust_main->invoice_terms;
2349 #use configured default or default default
2350 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2356 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2357 $duedate = $self->_date() + ( $1 * 86400 );
2364 $self->due_date ? time2str(shift, $self->due_date) : '';
2367 sub balance_due_msg {
2369 my $msg = 'Balance Due';
2370 return $msg unless $self->terms;
2371 if ( $self->due_date ) {
2372 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2373 } elsif ( $self->terms ) {
2374 $msg .= ' - '. $self->terms;
2381 my @display = scalar(@_)
2383 : qw( _items_previous _items_pkg );
2384 #: qw( _items_pkg );
2385 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2387 foreach my $display ( @display ) {
2388 push @b, $self->$display(@_);
2393 sub _items_previous {
2395 my $cust_main = $self->cust_main;
2396 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2398 foreach ( @pr_cust_bill ) {
2400 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2401 ' ('. time2str('%x',$_->_date). ')',
2402 #'pkgpart' => 'N/A',
2404 'amount' => sprintf("%.2f", $_->owed),
2410 # 'description' => 'Previous Balance',
2411 # #'pkgpart' => 'N/A',
2412 # 'pkgnum' => 'N/A',
2413 # 'amount' => sprintf("%10.2f", $pr_total ),
2414 # 'ext_description' => [ map {
2415 # "Invoice ". $_->invnum.
2416 # " (". time2str("%x",$_->_date). ") ".
2417 # sprintf("%10.2f", $_->owed)
2418 # } @pr_cust_bill ],
2425 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2426 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2431 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2432 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2435 sub _items_cust_bill_pkg {
2437 my $cust_bill_pkg = shift;
2440 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2442 my $desc = $cust_bill_pkg->desc;
2444 if ( $cust_bill_pkg->pkgnum > 0 ) {
2446 if ( $cust_bill_pkg->setup != 0 ) {
2447 my $description = $desc;
2448 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2449 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2450 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2452 description => $description,
2453 #pkgpart => $part_pkg->pkgpart,
2454 pkgnum => $cust_bill_pkg->pkgnum,
2455 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2456 ext_description => \@d,
2460 if ( $cust_bill_pkg->recur != 0 ) {
2462 description => $desc .
2463 ( $conf->exists('disable_line_item_date_ranges')
2465 : " (" .time2str("%x", $cust_bill_pkg->sdate).
2466 " - ".time2str("%x", $cust_bill_pkg->edate).")"
2468 #pkgpart => $part_pkg->pkgpart,
2469 pkgnum => $cust_bill_pkg->pkgnum,
2470 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2472 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2473 $cust_bill_pkg->sdate),
2474 $cust_bill_pkg->details,
2479 } else { #pkgnum tax or one-shot line item (??)
2481 if ( $cust_bill_pkg->setup != 0 ) {
2483 'description' => $desc,
2484 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2487 if ( $cust_bill_pkg->recur != 0 ) {
2489 'description' => "$desc (".
2490 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2491 time2str("%x", $cust_bill_pkg->edate). ')',
2492 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2504 sub _items_credits {
2509 foreach ( $self->cust_credited ) {
2511 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2513 my $reason = $_->cust_credit->reason;
2514 #my $reason = substr($_->cust_credit->reason,0,32);
2515 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2516 $reason = " ($reason) " if $reason;
2518 #'description' => 'Credit ref\#'. $_->crednum.
2519 # " (". time2str("%x",$_->cust_credit->_date) .")".
2521 'description' => 'Credit applied '.
2522 time2str("%x",$_->cust_credit->_date). $reason,
2523 'amount' => sprintf("%.2f",$_->amount),
2526 #foreach ( @cr_cust_credit ) {
2528 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2529 # $money_char. sprintf("%10.2f",$_->credited)
2537 sub _items_payments {
2541 #get & print payments
2542 foreach ( $self->cust_bill_pay ) {
2544 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2547 'description' => "Payment received ".
2548 time2str("%x",$_->cust_pay->_date ),
2549 'amount' => sprintf("%.2f", $_->amount )
2568 sub process_reprint {
2569 process_re_X('print', @_);
2576 sub process_reemail {
2577 process_re_X('email', @_);
2585 process_re_X('fax', @_);
2588 use Storable qw(thaw);
2592 my( $method, $job ) = ( shift, shift );
2593 warn "process_re_X $method for job $job\n" if $DEBUG;
2595 my $param = thaw(decode_base64(shift));
2596 warn Dumper($param) if $DEBUG;
2607 my($method, $job, %param ) = @_;
2608 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2610 warn "re_X $method for job $job with param:\n".
2611 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2614 #some false laziness w/search/cust_bill.html
2616 my $orderby = 'ORDER BY cust_bill._date';
2620 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2621 push @where, "cust_bill._date >= $1";
2623 if ( $param{'end'} =~ /^(\d+)$/ ) {
2624 push @where, "cust_bill._date < $1";
2626 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2627 push @where, "cust_main.agentnum = $1";
2631 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2632 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2633 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2634 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2636 push @where, "0 != $owed"
2639 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2642 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2644 my $addl_from = 'left join cust_main using ( custnum )';
2646 if ( $param{'newest_percust'} ) {
2647 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2648 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2649 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2652 my @cust_bill = qsearch( 'cust_bill',
2654 "$distinct cust_bill.*",
2660 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2661 foreach my $cust_bill ( @cust_bill ) {
2662 $cust_bill->$method();
2664 if ( $job ) { #progressbar foo
2666 if ( time - $min_sec > $last ) {
2667 my $error = $job->update_statustext(
2668 int( 100 * $num / scalar(@cust_bill) )
2670 die $error if $error;
2685 print_text formatting (and some logic :/) is in source, but needs to be
2686 slurped in from a file. Also number of lines ($=).
2690 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2691 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base