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);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main_Mixin;
20 use FS::cust_bill_pkg;
24 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
29 use FS::cust_bill_pay;
30 use FS::cust_bill_pay_batch;
31 use FS::part_bill_event;
34 @ISA = qw( FS::cust_main_Mixin FS::Record );
37 $me = '[FS::cust_bill]';
39 #ask FS::UID to run this stuff for us later
40 FS::UID->install_callback( sub {
42 $money_char = $conf->config('money_char') || '$';
47 FS::cust_bill - Object methods for cust_bill records
53 $record = new FS::cust_bill \%hash;
54 $record = new FS::cust_bill { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
66 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
68 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
70 @cust_pay_objects = $cust_bill->cust_pay;
72 $tax_amount = $record->tax;
74 @lines = $cust_bill->print_text;
75 @lines = $cust_bill->print_text $time;
79 An FS::cust_bill object represents an invoice; a declaration that a customer
80 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
81 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
82 following fields are currently supported:
86 =item invnum - primary key (assigned automatically for new invoices)
88 =item custnum - customer (see L<FS::cust_main>)
90 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
91 L<Time::Local> and L<Date::Parse> for conversion functions.
93 =item charged - amount of this invoice
95 =item printed - deprecated
97 =item closed - books closed flag, empty or `Y'
107 Creates a new invoice. To add the invoice to the database, see L<"insert">.
108 Invoices are normally created by calling the bill method of a customer object
109 (see L<FS::cust_main>).
113 sub table { 'cust_bill'; }
115 sub cust_linked { $_[0]->cust_main_custnum; }
116 sub cust_unlinked_msg {
118 "WARNING: can't find cust_main.custnum ". $self->custnum.
119 ' (cust_bill.invnum '. $self->invnum. ')';
124 Adds this invoice to the database ("Posts" the invoice). If there is an error,
125 returns the error, otherwise returns false.
129 This method now works but you probably shouldn't use it. Instead, apply a
130 credit against the invoice.
132 Using this method to delete invoices outright is really, really bad. There
133 would be no record you ever posted this invoice, and there are no check to
134 make sure charged = 0 or that there are no associated cust_bill_pkg records.
136 Really, don't use it.
142 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
143 $self->SUPER::delete(@_);
146 =item replace OLD_RECORD
148 Replaces the OLD_RECORD with this one in the database. If there is an error,
149 returns the error, otherwise returns false.
151 Only printed may be changed. printed is normally updated by calling the
152 collect method of a customer object (see L<FS::cust_main>).
156 #replace can be inherited from Record.pm
158 # replace_check is now the preferred way to #implement replace data checks
159 # (so $object->replace() works without an argument)
162 my( $new, $old ) = ( shift, shift );
163 return "Can't change custnum!" unless $old->custnum == $new->custnum;
164 #return "Can't change _date!" unless $old->_date eq $new->_date;
165 return "Can't change _date!" unless $old->_date == $new->_date;
166 return "Can't change charged!" unless $old->charged == $new->charged
167 || $old->charged == 0;
174 Checks all fields to make sure this is a valid invoice. If there is an error,
175 returns the error, otherwise returns false. Called by the insert and replace
184 $self->ut_numbern('invnum')
185 || $self->ut_number('custnum')
186 || $self->ut_numbern('_date')
187 || $self->ut_money('charged')
188 || $self->ut_numbern('printed')
189 || $self->ut_enum('closed', [ '', 'Y' ])
191 return $error if $error;
193 return "Unknown customer"
194 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
196 $self->_date(time) unless $self->_date;
198 $self->printed(0) if $self->printed eq '';
205 Returns a list consisting of the total previous balance for this customer,
206 followed by the previous outstanding invoices (as FS::cust_bill objects also).
213 my @cust_bill = sort { $a->_date <=> $b->_date }
214 grep { $_->owed != 0 && $_->_date < $self->_date }
215 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
217 foreach ( @cust_bill ) { $total += $_->owed; }
223 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
229 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
234 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
241 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
243 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
246 =item open_cust_bill_pkg
248 Returns the open line items for this invoice.
250 Note that cust_bill_pkg with both setup and recur fees are returned as two
251 separate line items, each with only one fee.
255 # modeled after cust_main::open_cust_bill
256 sub open_cust_bill_pkg {
259 # grep { $_->owed > 0 } $self->cust_bill_pkg
261 my %other = ( 'recur' => 'setup',
262 'setup' => 'recur', );
264 foreach my $field ( qw( recur setup )) {
265 push @open, map { $_->set( $other{$field}, 0 ); $_; }
266 grep { $_->owed($field) > 0 }
267 $self->cust_bill_pkg;
273 =item cust_bill_event
275 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
280 sub cust_bill_event {
282 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
288 Returns the customer (see L<FS::cust_main>) for this invoice.
294 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
297 =item cust_suspend_if_balance_over AMOUNT
299 Suspends the customer associated with this invoice if the total amount owed on
300 this invoice and all older invoices is greater than the specified amount.
302 Returns a list: an empty list on success or a list of errors.
306 sub cust_suspend_if_balance_over {
307 my( $self, $amount ) = ( shift, shift );
308 my $cust_main = $self->cust_main;
309 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
312 $cust_main->suspend(@_);
318 Depreciated. See the cust_credited method.
320 #Returns a list consisting of the total previous credited (see
321 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
322 #outstanding credits (FS::cust_credit objects).
328 croak "FS::cust_bill->cust_credit depreciated; see ".
329 "FS::cust_bill->cust_credit_bill";
332 #my @cust_credit = sort { $a->_date <=> $b->_date }
333 # grep { $_->credited != 0 && $_->_date < $self->_date }
334 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
336 #foreach (@cust_credit) { $total += $_->credited; }
337 #$total, @cust_credit;
342 Depreciated. See the cust_bill_pay method.
344 #Returns all payments (see L<FS::cust_pay>) for this invoice.
350 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
352 #sort { $a->_date <=> $b->_date }
353 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
359 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
365 sort { $a->_date <=> $b->_date }
366 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
371 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
377 sort { $a->_date <=> $b->_date }
378 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
384 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
391 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
393 foreach (@taxlines) { $total += $_->setup; }
399 Returns the amount owed (still outstanding) on this invoice, which is charged
400 minus all payment applications (see L<FS::cust_bill_pay>) and credit
401 applications (see L<FS::cust_credit_bill>).
407 my $balance = $self->charged;
408 $balance -= $_->amount foreach ( $self->cust_bill_pay );
409 $balance -= $_->amount foreach ( $self->cust_credited );
410 $balance = sprintf( "%.2f", $balance);
411 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
415 =item apply_payments_and_credits
419 sub apply_payments_and_credits {
422 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
423 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
425 while ( $self->owed > 0 and ( @payments || @credits ) ) {
428 if ( @payments && @credits ) {
430 #decide which goes first by weight of top (unapplied) line item
432 my @open_lineitems = $self->open_cust_bill_pkg;
435 max( map { $_->part_pkg->pay_weight || 0 }
440 my $max_credit_weight =
441 max( map { $_->part_pkg->credit_weight || 0 }
447 #if both are the same... payments first? it has to be something
448 if ( $max_pay_weight >= $max_credit_weight ) {
454 } elsif ( @payments ) {
456 } elsif ( @credits ) {
459 die "guru meditation #12 and 35";
462 if ( $app eq 'pay' ) {
464 my $payment = shift @payments;
466 $app = new FS::cust_bill_pay {
467 'paynum' => $payment->paynum,
468 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
471 } elsif ( $app eq 'credit' ) {
473 my $credit = shift @credits;
475 $app = new FS::cust_credit_bill {
476 'crednum' => $credit->crednum,
477 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
481 die "guru meditation #12 and 35";
484 $app->invnum( $self->invnum );
486 my $error = $app->insert;
487 die $error if $error;
493 =item generate_email PARAMHASH
495 PARAMHASH can contain the following:
499 =item from => sender address, required
501 =item tempate => alternate template name, optional
503 =item print_text => text attachment arrayref, optional
505 =item subject => email subject, optional
509 Returns an argument list to be passed to L<FS::Misc::send_email>.
520 my $me = '[FS::cust_bill::generate_email]';
523 'from' => $args{'from'},
524 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
527 if (ref($args{'to'} eq 'ARRAY')) {
528 $return{'to'} = $args{'to'};
530 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
531 $self->cust_main->invoicing_list
535 if ( $conf->exists('invoice_html') ) {
537 warn "$me creating HTML/text multipart message"
540 $return{'nobody'} = 1;
542 my $alternative = build MIME::Entity
543 'Type' => 'multipart/alternative',
544 'Encoding' => '7bit',
545 'Disposition' => 'inline'
549 if ( $conf->exists('invoice_email_pdf')
550 and scalar($conf->config('invoice_email_pdf_note')) ) {
552 warn "$me using 'invoice_email_pdf_note' in multipart message"
554 $data = [ map { $_ . "\n" }
555 $conf->config('invoice_email_pdf_note')
560 warn "$me not using 'invoice_email_pdf_note' in multipart message"
562 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
563 $data = $args{'print_text'};
565 $data = [ $self->print_text('', $args{'template'}) ];
570 $alternative->attach(
571 'Type' => 'text/plain',
572 #'Encoding' => 'quoted-printable',
573 'Encoding' => '7bit',
575 'Disposition' => 'inline',
578 $args{'from'} =~ /\@([\w\.\-]+)/;
579 my $from = $1 || 'example.com';
580 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
582 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
584 if ( defined($args{'template'}) && length($args{'template'})
585 && -e "$path/logo_". $args{'template'}. ".png"
588 $file = "$path/logo_". $args{'template'}. ".png";
590 $file = "$path/logo.png";
593 my $image = build MIME::Entity
594 'Type' => 'image/png',
595 'Encoding' => 'base64',
597 'Filename' => 'logo.png',
598 'Content-ID' => "<$content_id>",
601 $alternative->attach(
602 'Type' => 'text/html',
603 'Encoding' => 'quoted-printable',
604 'Data' => [ '<html>',
607 ' '. encode_entities($return{'subject'}),
610 ' <body bgcolor="#e8e8e8">',
611 $self->print_html('', $args{'template'}, $content_id),
615 'Disposition' => 'inline',
616 #'Filename' => 'invoice.pdf',
619 if ( $conf->exists('invoice_email_pdf') ) {
624 # multipart/alternative
630 my $related = build MIME::Entity 'Type' => 'multipart/related',
631 'Encoding' => '7bit';
633 #false laziness w/Misc::send_email
634 $related->head->replace('Content-type',
636 '; boundary="'. $related->head->multipart_boundary. '"'.
637 '; type=multipart/alternative'
640 $related->add_part($alternative);
642 $related->add_part($image);
644 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
646 $return{'mimeparts'} = [ $related, $pdf ];
650 #no other attachment:
652 # multipart/alternative
657 $return{'content-type'} = 'multipart/related';
658 $return{'mimeparts'} = [ $alternative, $image ];
659 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
660 #$return{'disposition'} = 'inline';
666 if ( $conf->exists('invoice_email_pdf') ) {
667 warn "$me creating PDF attachment"
670 #mime parts arguments a la MIME::Entity->build().
671 $return{'mimeparts'} = [
672 { $self->mimebuild_pdf('', $args{'template'}) }
676 if ( $conf->exists('invoice_email_pdf')
677 and scalar($conf->config('invoice_email_pdf_note')) ) {
679 warn "$me using 'invoice_email_pdf_note'"
681 $return{'body'} = [ map { $_ . "\n" }
682 $conf->config('invoice_email_pdf_note')
687 warn "$me not using 'invoice_email_pdf_note'"
689 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
690 $return{'body'} = $args{'print_text'};
692 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
705 Returns a list suitable for passing to MIME::Entity->build(), representing
706 this invoice as PDF attachment.
713 'Type' => 'application/pdf',
714 'Encoding' => 'base64',
715 'Data' => [ $self->print_pdf(@_) ],
716 'Disposition' => 'attachment',
717 'Filename' => 'invoice.pdf',
721 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
723 Sends this invoice to the destinations configured for this customer: sends
724 email, prints and/or faxes. See L<FS::cust_main_invoice>.
726 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
728 AGENTNUM, if specified, means that this invoice will only be sent for customers
729 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
730 single agent) or an arrayref of agentnums.
732 INVOICE_FROM, if specified, overrides the default email invoice From: address.
739 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
740 or die "invalid invoice number: " . $opt{invnum};
742 my @args = ( $opt{template}, $opt{agentnum} );
743 push @args, $opt{invoice_from}
744 if exists($opt{invoice_from}) && $opt{invoice_from};
746 my $error = $self->send( @args );
747 die $error if $error;
753 my $template = scalar(@_) ? shift : '';
754 if ( scalar(@_) && $_[0] ) {
755 my $agentnums = ref($_[0]) ? shift : [ shift ];
756 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
762 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
764 my @invoicing_list = $self->cust_main->invoicing_list;
766 $self->email($template, $invoice_from)
767 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
769 $self->print($template)
770 if grep { $_ eq 'POST' } @invoicing_list; #postal
772 $self->fax($template)
773 if grep { $_ eq 'FAX' } @invoicing_list; #fax
779 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
783 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
785 INVOICE_FROM, if specified, overrides the default email invoice From: address.
789 sub queueable_email {
792 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
793 or die "invalid invoice number: " . $opt{invnum};
795 my @args = ( $opt{template} );
796 push @args, $opt{invoice_from}
797 if exists($opt{invoice_from}) && $opt{invoice_from};
799 my $error = $self->email( @args );
800 die $error if $error;
806 my $template = scalar(@_) ? shift : '';
810 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
812 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
813 $self->cust_main->invoicing_list;
815 #better to notify this person than silence
816 @invoicing_list = ($invoice_from) unless @invoicing_list;
818 my $error = send_email(
819 $self->generate_email(
820 'from' => $invoice_from,
821 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
822 'template' => $template,
825 die "can't email invoice: $error\n" if $error;
826 #die "$error\n" if $error;
830 =item lpr_data [ TEMPLATENAME ]
832 Returns the postscript or plaintext for this invoice as an arrayref.
834 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
839 my( $self, $template) = @_;
840 $conf->exists('invoice_latex')
841 ? [ $self->print_ps('', $template) ]
842 : [ $self->print_text('', $template) ];
845 =item print [ TEMPLATENAME ]
849 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
855 my $template = scalar(@_) ? shift : '';
857 my $lpr = $conf->config('lpr');
860 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
862 $outerr = ": $outerr" if length($outerr);
863 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
868 =item fax [ TEMPLATENAME ]
872 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
878 my $template = scalar(@_) ? shift : '';
880 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
881 unless $conf->exists('invoice_latex');
883 my $dialstring = $self->cust_main->getfield('fax');
886 my $error = send_fax( 'docdata' => $self->lpr_data($template),
887 'dialstring' => $dialstring,
889 die $error if $error;
893 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
895 Like B<send>, but only sends the invoice if it is the newest open invoice for
905 grep { $_->owed > 0 }
906 qsearch('cust_bill', {
907 'custnum' => $self->custnum,
908 #'_date' => { op=>'>', value=>$self->_date },
909 'invnum' => { op=>'>', value=>$self->invnum },
916 =item send_csv OPTION => VALUE, ...
918 Sends invoice as a CSV data-file to a remote host with the specified protocol.
922 protocol - currently only "ftp"
928 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
929 and YYMMDDHHMMSS is a timestamp.
931 See L</print_csv> for a description of the output format.
936 my($self, %opt) = @_;
940 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
941 mkdir $spooldir, 0700 unless -d $spooldir;
943 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
944 my $file = "$spooldir/$tracctnum.csv";
946 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
948 open(CSV, ">$file") or die "can't open $file: $!";
956 if ( $opt{protocol} eq 'ftp' ) {
957 eval "use Net::FTP;";
959 $net = Net::FTP->new($opt{server}) or die @$;
961 die "unknown protocol: $opt{protocol}";
964 $net->login( $opt{username}, $opt{password} )
965 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
967 $net->binary or die "can't set binary mode";
969 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
971 $net->put($file) or die "can't put $file: $!";
981 Spools CSV invoice data.
987 =item format - 'default' or 'billco'
989 =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>).
991 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
993 =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.
1000 my($self, %opt) = @_;
1002 my $cust_main = $self->cust_main;
1004 if ( $opt{'dest'} ) {
1005 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1006 $cust_main->invoicing_list;
1007 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1008 || ! keys %invoicing_list;
1011 if ( $opt{'balanceover'} ) {
1013 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1016 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1017 mkdir $spooldir, 0700 unless -d $spooldir;
1019 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1023 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1024 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1027 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1029 open(CSV, ">>$file") or die "can't open $file: $!";
1030 flock(CSV, LOCK_EX);
1035 if ( lc($opt{'format'}) eq 'billco' ) {
1037 flock(CSV, LOCK_UN);
1042 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1045 open(CSV,">>$file") or die "can't open $file: $!";
1046 flock(CSV, LOCK_EX);
1052 flock(CSV, LOCK_UN);
1059 =item print_csv OPTION => VALUE, ...
1061 Returns CSV data for this invoice.
1065 format - 'default' or 'billco'
1067 Returns a list consisting of two scalars. The first is a single line of CSV
1068 header information for this invoice. The second is one or more lines of CSV
1069 detail information for this invoice.
1071 If I<format> is not specified or "default", the fields of the CSV file are as
1074 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1078 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1080 B<record_type> is C<cust_bill> for the initial header line only. The
1081 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1082 fields are filled in.
1084 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1085 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1088 =item invnum - invoice number
1090 =item custnum - customer number
1092 =item _date - invoice date
1094 =item charged - total invoice amount
1096 =item first - customer first name
1098 =item last - customer first name
1100 =item company - company name
1102 =item address1 - address line 1
1104 =item address2 - address line 1
1114 =item pkg - line item description
1116 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1118 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1120 =item sdate - start date for recurring fee
1122 =item edate - end date for recurring fee
1126 If I<format> is "billco", the fields of the header CSV file are as follows:
1128 +-------------------------------------------------------------------+
1129 | FORMAT HEADER FILE |
1130 |-------------------------------------------------------------------|
1131 | Field | Description | Name | Type | Width |
1132 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1133 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1134 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1135 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1136 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1137 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1138 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1139 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1140 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1141 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1142 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1143 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1144 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1145 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1146 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1147 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1148 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1149 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1150 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1151 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1152 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1153 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1154 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1155 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1156 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1157 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1158 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1159 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1160 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1161 +-------+-------------------------------+------------+------+-------+
1163 If I<format> is "billco", the fields of the detail CSV file are as follows:
1165 FORMAT FOR DETAIL FILE
1167 Field | Description | Name | Type | Width
1168 1 | N/A-Leave Empty | RC | CHAR | 2
1169 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1170 3 | Account Number | TRACCTNUM | CHAR | 15
1171 4 | Invoice Number | TRINVOICE | CHAR | 15
1172 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1173 6 | Transaction Detail | DETAILS | CHAR | 100
1174 7 | Amount | AMT | NUM* | 9
1175 8 | Line Format Control** | LNCTRL | CHAR | 2
1176 9 | Grouping Code | GROUP | CHAR | 2
1177 10 | User Defined | ACCT CODE | CHAR | 15
1182 my($self, %opt) = @_;
1184 eval "use Text::CSV_XS";
1187 my $cust_main = $self->cust_main;
1189 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1191 if ( lc($opt{'format'}) eq 'billco' ) {
1194 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1197 if ( $conf->exists('invoice_default_terms')
1198 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1199 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1202 my( $previous_balance, @unused ) = $self->previous; #previous balance
1204 my $pmt_cr_applied = 0;
1205 $pmt_cr_applied += $_->{'amount'}
1206 foreach ( $self->_items_payments, $self->_items_credits ) ;
1208 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1211 '', # 1 | N/A-Leave Empty CHAR 2
1212 '', # 2 | N/A-Leave Empty CHAR 15
1213 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1214 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1215 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1216 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1217 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1218 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1219 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1220 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1221 '', # 10 | Ancillary Billing Information CHAR 30
1222 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1223 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1226 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1229 $duedate, # 14 | Bill Due Date CHAR 10
1231 $previous_balance, # 15 | Previous Balance NUM* 9
1232 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1233 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1234 $totaldue, # 18 | Total Amt Due NUM* 9
1235 $totaldue, # 19 | Total Amt Due NUM* 9
1236 '', # 20 | 30 Day Aging NUM* 9
1237 '', # 21 | 60 Day Aging NUM* 9
1238 '', # 22 | 90 Day Aging NUM* 9
1239 'N', # 23 | Y/N CHAR 1
1240 '', # 24 | Remittance automation CHAR 100
1241 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1242 $self->custnum, # 26 | Customer Reference Number CHAR 15
1243 '0', # 27 | Federal Tax*** NUM* 9
1244 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1245 '0', # 29 | Other Taxes & Fees*** NUM* 9
1254 time2str("%x", $self->_date),
1255 sprintf("%.2f", $self->charged),
1256 ( map { $cust_main->getfield($_) }
1257 qw( first last company address1 address2 city state zip country ) ),
1259 ) or die "can't create csv";
1262 my $header = $csv->string. "\n";
1265 if ( lc($opt{'format'}) eq 'billco' ) {
1268 foreach my $item ( $self->_items_pkg ) {
1271 '', # 1 | N/A-Leave Empty CHAR 2
1272 '', # 2 | N/A-Leave Empty CHAR 15
1273 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1274 $self->invnum, # 4 | Invoice Number CHAR 15
1275 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1276 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1277 $item->{'amount'}, # 7 | Amount NUM* 9
1278 '', # 8 | Line Format Control** CHAR 2
1279 '', # 9 | Grouping Code CHAR 2
1280 '', # 10 | User Defined CHAR 15
1283 $detail .= $csv->string. "\n";
1289 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1291 my($pkg, $setup, $recur, $sdate, $edate);
1292 if ( $cust_bill_pkg->pkgnum ) {
1294 ($pkg, $setup, $recur, $sdate, $edate) = (
1295 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1296 ( $cust_bill_pkg->setup != 0
1297 ? sprintf("%.2f", $cust_bill_pkg->setup )
1299 ( $cust_bill_pkg->recur != 0
1300 ? sprintf("%.2f", $cust_bill_pkg->recur )
1302 ( $cust_bill_pkg->sdate
1303 ? time2str("%x", $cust_bill_pkg->sdate)
1305 ($cust_bill_pkg->edate
1306 ?time2str("%x", $cust_bill_pkg->edate)
1310 } else { #pkgnum tax
1311 next unless $cust_bill_pkg->setup != 0;
1312 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1313 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1315 ($pkg, $setup, $recur, $sdate, $edate) =
1316 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1322 ( map { '' } (1..11) ),
1323 ($pkg, $setup, $recur, $sdate, $edate)
1324 ) or die "can't create csv";
1326 $detail .= $csv->string. "\n";
1332 ( $header, $detail );
1338 Pays this invoice with a compliemntary payment. If there is an error,
1339 returns the error, otherwise returns false.
1345 my $cust_pay = new FS::cust_pay ( {
1346 'invnum' => $self->invnum,
1347 'paid' => $self->owed,
1350 'payinfo' => $self->cust_main->payinfo,
1358 Attempts to pay this invoice with a credit card payment via a
1359 Business::OnlinePayment realtime gateway. See
1360 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1361 for supported processors.
1367 $self->realtime_bop( 'CC', @_ );
1372 Attempts to pay this invoice with an electronic check (ACH) payment via a
1373 Business::OnlinePayment realtime gateway. See
1374 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1375 for supported processors.
1381 $self->realtime_bop( 'ECHECK', @_ );
1386 Attempts to pay this invoice with phone bill (LEC) payment via a
1387 Business::OnlinePayment realtime gateway. See
1388 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1389 for supported processors.
1395 $self->realtime_bop( 'LEC', @_ );
1399 my( $self, $method ) = @_;
1401 my $cust_main = $self->cust_main;
1402 my $balance = $cust_main->balance;
1403 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1404 $amount = sprintf("%.2f", $amount);
1405 return "not run (balance $balance)" unless $amount > 0;
1407 my $description = 'Internet Services';
1408 if ( $conf->exists('business-onlinepayment-description') ) {
1409 my $dtempl = $conf->config('business-onlinepayment-description');
1411 my $agent_obj = $cust_main->agent
1412 or die "can't retreive agent for $cust_main (agentnum ".
1413 $cust_main->agentnum. ")";
1414 my $agent = $agent_obj->agent;
1415 my $pkgs = join(', ',
1416 map { $_->cust_pkg->part_pkg->pkg }
1417 grep { $_->pkgnum } $self->cust_bill_pkg
1419 $description = eval qq("$dtempl");
1422 $cust_main->realtime_bop($method, $amount,
1423 'description' => $description,
1424 'invnum' => $self->invnum,
1429 =item batch_card OPTION => VALUE...
1431 Adds a payment for this invoice to the pending credit card batch (see
1432 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1433 runs the payment using a realtime gateway.
1438 my ($self, %options) = @_;
1439 my $cust_main = $self->cust_main;
1441 my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1442 return '' unless $amount > 0;
1444 if ($options{'realtime'}) {
1445 return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1451 my $oldAutoCommit = $FS::UID::AutoCommit;
1452 local $FS::UID::AutoCommit = 0;
1455 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1456 or return "Cannot lock pay_batch: " . $dbh->errstr;
1460 'payby' => FS::payby->payby2payment($cust_main->payby),
1463 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1465 unless ( $pay_batch ) {
1466 $pay_batch = new FS::pay_batch \%pay_batch;
1467 my $error = $pay_batch->insert;
1469 $dbh->rollback if $oldAutoCommit;
1470 die "error creating new batch: $error\n";
1474 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1475 'batchnum' => $pay_batch->batchnum,
1476 'custnum' => $cust_main->custnum,
1479 my $cust_pay_batch = new FS::cust_pay_batch ( {
1480 'batchnum' => $pay_batch->batchnum,
1481 'invnum' => $self->getfield('invnum'), # is there a better value?
1482 # this field should be
1484 # cust_bill_pay_batch now
1485 'custnum' => $cust_main->custnum,
1486 'last' => $cust_main->getfield('last'),
1487 'first' => $cust_main->getfield('first'),
1488 'address1' => $cust_main->address1,
1489 'address2' => $cust_main->address2,
1490 'city' => $cust_main->city,
1491 'state' => $cust_main->state,
1492 'zip' => $cust_main->zip,
1493 'country' => $cust_main->country,
1494 'payby' => $cust_main->payby,
1495 'payinfo' => $cust_main->payinfo,
1496 'exp' => $cust_main->paydate,
1497 'payname' => $cust_main->payname,
1498 'amount' => $amount, # consolidating
1501 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1502 if $old_cust_pay_batch;
1505 if ($old_cust_pay_batch) {
1506 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1508 $error = $cust_pay_batch->insert;
1512 $dbh->rollback if $oldAutoCommit;
1516 my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1517 foreach my $cust_bill ($cust_main->open_cust_bill) {
1518 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1519 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1520 'invnum' => $cust_bill->invnum,
1521 'paybatchnum' => $cust_pay_batch->paybatchnum,
1522 'amount' => $cust_bill->owed,
1525 if ($unapplied >= $cust_bill_pay_batch->amount){
1526 $unapplied -= $cust_bill_pay_batch->amount;
1529 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
1530 $cust_bill_pay_batch->amount - $unapplied ));
1533 $error = $cust_bill_pay_batch->insert;
1535 $dbh->rollback if $oldAutoCommit;
1540 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1544 sub _agent_template {
1546 $self->_agent_plandata('agent_templatename');
1549 sub _agent_invoice_from {
1551 $self->_agent_plandata('agent_invoice_from');
1554 sub _agent_plandata {
1555 my( $self, $option ) = @_;
1557 my $part_bill_event = qsearchs( 'part_bill_event',
1559 'payby' => $self->cust_main->payby,
1560 'plan' => 'send_agent',
1561 'plandata' => { 'op' => '~',
1562 'value' => "(^|\n)agentnum ".
1564 $self->cust_main->agentnum.
1570 'ORDER BY seconds LIMIT 1'
1573 return '' unless $part_bill_event;
1575 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1578 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1579 " plandata for $option";
1585 =item print_text [ TIME [ , TEMPLATE ] ]
1587 Returns an text invoice, as a list of lines.
1589 TIME an optional value used to control the printing of overdue messages. The
1590 default is now. It isn't the date of the invoice; that's the `_date' field.
1591 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1592 L<Time::Local> and L<Date::Parse> for conversion functions.
1596 #still some false laziness w/_items stuff (and send_csv)
1599 my( $self, $today, $template ) = @_;
1602 # my $invnum = $self->invnum;
1603 my $cust_main = $self->cust_main;
1604 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1605 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1607 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1608 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1609 #my $balance_due = $self->owed + $pr_total - $cr_total;
1610 my $balance_due = $self->owed + $pr_total;
1613 #my($description,$amount);
1617 foreach ( @pr_cust_bill ) {
1619 "Previous Balance, Invoice #". $_->invnum.
1620 " (". time2str("%x",$_->_date). ")",
1621 $money_char. sprintf("%10.2f",$_->owed)
1624 if (@pr_cust_bill) {
1625 push @buf,['','-----------'];
1626 push @buf,[ 'Total Previous Balance',
1627 $money_char. sprintf("%10.2f",$pr_total ) ];
1632 foreach my $cust_bill_pkg (
1633 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1634 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1637 my $desc = $cust_bill_pkg->desc;
1639 if ( $cust_bill_pkg->pkgnum > 0 ) {
1641 if ( $cust_bill_pkg->setup != 0 ) {
1642 my $description = $desc;
1643 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1644 push @buf, [ $description,
1645 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1647 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1648 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1651 if ( $cust_bill_pkg->recur != 0 ) {
1653 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1654 time2str("%x", $cust_bill_pkg->edate) . ")",
1655 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1658 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1659 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1660 $cust_bill_pkg->sdate );
1663 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1665 } else { #pkgnum tax or one-shot line item
1667 if ( $cust_bill_pkg->setup != 0 ) {
1669 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1671 if ( $cust_bill_pkg->recur != 0 ) {
1672 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1673 . time2str("%x", $cust_bill_pkg->edate). ")",
1674 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1682 push @buf,['','-----------'];
1683 push @buf,['Total New Charges',
1684 $money_char. sprintf("%10.2f",$self->charged) ];
1687 push @buf,['','-----------'];
1688 push @buf,['Total Charges',
1689 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1693 foreach ( $self->cust_credited ) {
1695 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1697 my $reason = substr($_->cust_credit->reason,0,32);
1698 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1699 $reason = " ($reason) " if $reason;
1701 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1703 $money_char. sprintf("%10.2f",$_->amount)
1706 #foreach ( @cr_cust_credit ) {
1708 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1709 # $money_char. sprintf("%10.2f",$_->credited)
1713 #get & print payments
1714 foreach ( $self->cust_bill_pay ) {
1716 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1719 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1720 $money_char. sprintf("%10.2f",$_->amount )
1725 my $balance_due_msg = $self->balance_due_msg;
1727 push @buf,['','-----------'];
1728 push @buf,[$balance_due_msg, $money_char.
1729 sprintf("%10.2f", $balance_due ) ];
1731 #create the template
1732 $template ||= $self->_agent_template;
1733 my $templatefile = 'invoice_template';
1734 $templatefile .= "_$template" if length($template);
1735 my @invoice_template = $conf->config($templatefile)
1736 or die "cannot load config file $templatefile";
1739 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1740 /invoice_lines\((\d*)\)/;
1741 $invoice_lines += $1 || scalar(@buf);
1744 die "no invoice_lines() functions in template?" unless $wasfunc;
1745 my $invoice_template = new Text::Template (
1747 SOURCE => [ map "$_\n", @invoice_template ],
1748 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1749 $invoice_template->compile()
1750 or die "can't compile template: $Text::Template::ERROR";
1752 #setup template variables
1753 package FS::cust_bill::_template; #!
1754 use vars qw( $custnum $invnum $date $agent @address $overdue
1755 $page $total_pages @buf );
1757 $custnum = $self->custnum;
1758 $invnum = $self->invnum;
1759 $date = $self->_date;
1760 $agent = $self->cust_main->agent->agent;
1763 if ( $FS::cust_bill::invoice_lines ) {
1765 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1767 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1772 #format address (variable for the template)
1774 @address = ( '', '', '', '', '', '' );
1775 package FS::cust_bill; #!
1776 $FS::cust_bill::_template::address[$l++] =
1777 $cust_main->payname.
1778 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1779 ? " (P.O. #". $cust_main->payinfo. ")"
1783 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1784 if $cust_main->company;
1785 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1786 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1787 if $cust_main->address2;
1788 $FS::cust_bill::_template::address[$l++] =
1789 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1791 my $countrydefault = $conf->config('countrydefault') || 'US';
1792 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1793 unless $cust_main->country eq $countrydefault;
1795 # #overdue? (variable for the template)
1796 # $FS::cust_bill::_template::overdue = (
1798 # && $today > $self->_date
1799 ## && $self->printed > 1
1800 # && $self->printed > 0
1803 #and subroutine for the template
1804 sub FS::cust_bill::_template::invoice_lines {
1805 my $lines = shift || scalar(@buf);
1807 scalar(@buf) ? shift @buf : [ '', '' ];
1813 $FS::cust_bill::_template::page = 1;
1817 push @collect, split("\n",
1818 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1820 $FS::cust_bill::_template::page++;
1823 map "$_\n", @collect;
1827 =item print_latex [ TIME [ , TEMPLATE ] ]
1829 Internal method - returns a filename of a filled-in LaTeX template for this
1830 invoice (Note: add ".tex" to get the actual filename), and a filename of
1831 an associated logo (with the .eps extension included).
1833 See print_ps and print_pdf for methods that return PostScript and PDF output.
1835 TIME an optional value used to control the printing of overdue messages. The
1836 default is now. It isn't the date of the invoice; that's the `_date' field.
1837 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1838 L<Time::Local> and L<Date::Parse> for conversion functions.
1842 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1845 my( $self, $today, $template ) = @_;
1847 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1850 my $cust_main = $self->cust_main;
1851 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1852 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1854 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1855 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1856 #my $balance_due = $self->owed + $pr_total - $cr_total;
1857 my $balance_due = $self->owed + $pr_total;
1859 #create the template
1860 $template ||= $self->_agent_template;
1861 my $templatefile = 'invoice_latex';
1862 my $suffix = length($template) ? "_$template" : '';
1863 $templatefile .= $suffix;
1864 my @invoice_template = map "$_\n", $conf->config($templatefile)
1865 or die "cannot load config file $templatefile";
1867 my($format, $text_template);
1868 if ( grep { /^%%Detail/ } @invoice_template ) {
1869 #change this to a die when the old code is removed
1870 warn "old-style invoice template $templatefile; ".
1871 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1874 $format = 'Text::Template';
1875 $text_template = new Text::Template(
1877 SOURCE => \@invoice_template,
1878 DELIMITERS => [ '[@--', '--@]' ],
1881 $text_template->compile()
1882 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1886 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1887 $returnaddress = join("\n",
1888 $conf->config_orbase('invoice_latexreturnaddress', $template)
1891 $returnaddress = '~';
1894 my %invoice_data = (
1895 'custnum' => $self->custnum,
1896 'invnum' => $self->invnum,
1897 'date' => time2str('%b %o, %Y', $self->_date),
1898 'today' => time2str('%b %o, %Y', $today),
1899 'agent' => _latex_escape($cust_main->agent->agent),
1900 'payname' => _latex_escape($cust_main->payname),
1901 'company' => _latex_escape($cust_main->company),
1902 'address1' => _latex_escape($cust_main->address1),
1903 'address2' => _latex_escape($cust_main->address2),
1904 'city' => _latex_escape($cust_main->city),
1905 'state' => _latex_escape($cust_main->state),
1906 'zip' => _latex_escape($cust_main->zip),
1907 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1908 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1909 'returnaddress' => $returnaddress,
1911 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1912 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1913 # better hang on to conf_dir for a while
1914 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1917 my $countrydefault = $conf->config('countrydefault') || 'US';
1918 if ( $cust_main->country eq $countrydefault ) {
1919 $invoice_data{'country'} = '';
1921 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1924 $invoice_data{'notes'} =
1926 # #do variable substitutions in notes
1927 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1928 $conf->config_orbase('invoice_latexnotes', $template)
1930 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1933 $invoice_data{'footer'} =~ s/\n+$//;
1934 $invoice_data{'smallfooter'} =~ s/\n+$//;
1935 $invoice_data{'notes'} =~ s/\n+$//;
1937 $invoice_data{'po_line'} =
1938 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1939 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1943 if ( $format eq 'old' ) {
1946 my @total_item = ();
1947 while ( @invoice_template ) {
1948 my $line = shift @invoice_template;
1950 if ( $line =~ /^%%Detail\s*$/ ) {
1952 while ( ( my $line_item_line = shift @invoice_template )
1953 !~ /^%%EndDetail\s*$/ ) {
1954 push @line_item, $line_item_line;
1956 foreach my $line_item ( $self->_items ) {
1957 #foreach my $line_item ( $self->_items_pkg ) {
1958 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1959 $invoice_data{'description'} =
1960 _latex_escape($line_item->{'description'});
1961 if ( exists $line_item->{'ext_description'} ) {
1962 $invoice_data{'description'} .=
1963 "\\tabularnewline\n~~".
1964 join( "\\tabularnewline\n~~",
1965 map _latex_escape($_), @{$line_item->{'ext_description'}}
1968 $invoice_data{'amount'} = $line_item->{'amount'};
1969 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1971 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1974 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1976 while ( ( my $total_item_line = shift @invoice_template )
1977 !~ /^%%EndTotalDetails\s*$/ ) {
1978 push @total_item, $total_item_line;
1981 my @total_fill = ();
1984 foreach my $tax ( $self->_items_tax ) {
1985 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1986 $taxtotal += $tax->{'amount'};
1987 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1989 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1994 $invoice_data{'total_item'} = 'Sub-total';
1995 $invoice_data{'total_amount'} =
1996 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1997 unshift @total_fill,
1998 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2002 $invoice_data{'total_item'} = '\textbf{Total}';
2003 $invoice_data{'total_amount'} =
2004 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2006 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2009 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2012 foreach my $credit ( $self->_items_credits ) {
2013 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2015 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2017 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2022 foreach my $payment ( $self->_items_payments ) {
2023 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2025 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2027 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2031 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2032 $invoice_data{'total_amount'} =
2033 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2035 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2038 push @filled_in, @total_fill;
2041 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2042 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2043 push @filled_in, $line;
2054 } elsif ( $format eq 'Text::Template' ) {
2056 my @detail_items = ();
2057 my @total_items = ();
2059 $invoice_data{'detail_items'} = \@detail_items;
2060 $invoice_data{'total_items'} = \@total_items;
2062 foreach my $line_item ( $self->_items ) {
2064 ext_description => [],
2066 $detail->{'ref'} = $line_item->{'pkgnum'};
2067 $detail->{'quantity'} = 1;
2068 $detail->{'description'} = _latex_escape($line_item->{'description'});
2069 if ( exists $line_item->{'ext_description'} ) {
2070 @{$detail->{'ext_description'}} = map {
2072 } @{$line_item->{'ext_description'}};
2074 $detail->{'amount'} = $line_item->{'amount'};
2075 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2077 push @detail_items, $detail;
2082 foreach my $tax ( $self->_items_tax ) {
2084 $total->{'total_item'} = _latex_escape($tax->{'description'});
2085 $taxtotal += $tax->{'amount'};
2086 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2087 push @total_items, $total;
2092 $total->{'total_item'} = 'Sub-total';
2093 $total->{'total_amount'} =
2094 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2095 unshift @total_items, $total;
2100 $total->{'total_item'} = '\textbf{Total}';
2101 $total->{'total_amount'} =
2102 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2103 push @total_items, $total;
2106 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2109 foreach my $credit ( $self->_items_credits ) {
2111 $total->{'total_item'} = _latex_escape($credit->{'description'});
2113 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2114 push @total_items, $total;
2118 foreach my $payment ( $self->_items_payments ) {
2120 $total->{'total_item'} = _latex_escape($payment->{'description'});
2122 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2123 push @total_items, $total;
2128 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2129 $total->{'total_amount'} =
2130 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2131 push @total_items, $total;
2135 die "guru meditation #54";
2138 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2139 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2143 ) or die "can't open temp file: $!\n";
2145 if ($template && $conf->exists("logo_${template}.eps")) {
2146 print $lh $conf->config_binary("logo_${template}.eps")
2147 or die "can't write temp file: $!\n";
2149 print $lh $conf->config_binary('logo.eps')
2150 or die "can't write temp file: $!\n";
2153 $invoice_data{'logo_file'} = $lh->filename;
2155 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2159 ) or die "can't open temp file: $!\n";
2160 if ( $format eq 'old' ) {
2161 print $fh join('', @filled_in );
2162 } elsif ( $format eq 'Text::Template' ) {
2163 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2165 die "guru meditation #32";
2169 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2170 return ($1, $invoice_data{'logo_file'});
2174 =item print_ps [ TIME [ , TEMPLATE ] ]
2176 Returns an postscript invoice, as a scalar.
2178 TIME an optional value used to control the printing of overdue messages. The
2179 default is now. It isn't the date of the invoice; that's the `_date' field.
2180 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2181 L<Time::Local> and L<Date::Parse> for conversion functions.
2188 my ($file, $lfile) = $self->print_latex(@_);
2190 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2193 my $sfile = shell_quote $file;
2195 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2196 or die "pslatex $file.tex failed; see $file.log for details?\n";
2197 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2198 or die "pslatex $file.tex failed; see $file.log for details?\n";
2200 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2201 or die "dvips failed";
2203 open(POSTSCRIPT, "<$file.ps")
2204 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2206 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2210 while (<POSTSCRIPT>) {
2220 =item print_pdf [ TIME [ , TEMPLATE ] ]
2222 Returns an PDF invoice, as a scalar.
2224 TIME an optional value used to control the printing of overdue messages. The
2225 default is now. It isn't the date of the invoice; that's the `_date' field.
2226 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2227 L<Time::Local> and L<Date::Parse> for conversion functions.
2234 my ($file, $lfile) = $self->print_latex(@_);
2236 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2239 #system('pdflatex', "$file.tex");
2240 #system('pdflatex', "$file.tex");
2241 #! LaTeX Error: Unknown graphics extension: .eps.
2243 my $sfile = shell_quote $file;
2245 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2246 or die "pslatex $file.tex failed; see $file.log for details?\n";
2247 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2248 or die "pslatex $file.tex failed; see $file.log for details?\n";
2250 #system('dvipdf', "$file.dvi", "$file.pdf" );
2252 "dvips -q -t letter -f $sfile.dvi ".
2253 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2256 or die "dvips | gs failed: $!";
2258 open(PDF, "<$file.pdf")
2259 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2261 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2275 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2277 Returns an HTML invoice, as a scalar.
2279 TIME an optional value used to control the printing of overdue messages. The
2280 default is now. It isn't the date of the invoice; that's the `_date' field.
2281 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2282 L<Time::Local> and L<Date::Parse> for conversion functions.
2284 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2285 when emailing the invoice as part of a multipart/related MIME email.
2289 #some falze laziness w/print_text and print_latex (and send_csv)
2291 my( $self, $today, $template, $cid ) = @_;
2294 my $cust_main = $self->cust_main;
2295 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2296 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2298 $template ||= $self->_agent_template;
2299 my $templatefile = 'invoice_html';
2300 my $suffix = length($template) ? "_$template" : '';
2301 $templatefile .= $suffix;
2302 my @html_template = map "$_\n", $conf->config($templatefile)
2303 or die "cannot load config file $templatefile";
2305 my $html_template = new Text::Template(
2307 SOURCE => \@html_template,
2308 DELIMITERS => [ '<%=', '%>' ],
2311 $html_template->compile()
2312 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2314 my %invoice_data = (
2315 'custnum' => $self->custnum,
2316 'invnum' => $self->invnum,
2317 'date' => time2str('%b %o, %Y', $self->_date),
2318 'today' => time2str('%b %o, %Y', $today),
2319 'agent' => encode_entities($cust_main->agent->agent),
2320 'payname' => encode_entities($cust_main->payname),
2321 'company' => encode_entities($cust_main->company),
2322 'address1' => encode_entities($cust_main->address1),
2323 'address2' => encode_entities($cust_main->address2),
2324 'city' => encode_entities($cust_main->city),
2325 'state' => encode_entities($cust_main->state),
2326 'zip' => encode_entities($cust_main->zip),
2327 'terms' => $conf->config('invoice_default_terms')
2328 || 'Payable upon receipt',
2330 'template' => $template,
2331 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2335 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2336 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2338 $invoice_data{'returnaddress'} =
2339 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2341 $invoice_data{'returnaddress'} =
2344 s/\\\\\*?\s*$/<BR>/;
2345 s/\\hyphenation\{[\w\s\-]+\}//;
2348 $conf->config_orbase( 'invoice_latexreturnaddress',
2354 my $countrydefault = $conf->config('countrydefault') || 'US';
2355 if ( $cust_main->country eq $countrydefault ) {
2356 $invoice_data{'country'} = '';
2358 $invoice_data{'country'} =
2359 encode_entities(code2country($cust_main->country));
2363 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2364 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2366 $invoice_data{'notes'} =
2367 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2369 $invoice_data{'notes'} =
2371 s/%%(.*)$/<!-- $1 -->/;
2372 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2373 s/\\begin\{enumerate\}/<ol>/;
2375 s/\\end\{enumerate\}/<\/ol>/;
2376 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2380 $conf->config_orbase('invoice_latexnotes', $template)
2384 # #do variable substitutions in notes
2385 # $invoice_data{'notes'} =
2387 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2388 # $conf->config_orbase('invoice_latexnotes', $suffix)
2392 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2393 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2395 $invoice_data{'footer'} =
2396 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2398 $invoice_data{'footer'} =
2399 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2400 $conf->config_orbase('invoice_latexfooter', $template)
2404 $invoice_data{'po_line'} =
2405 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2406 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2409 my $money_char = $conf->config('money_char') || '$';
2411 foreach my $line_item ( $self->_items ) {
2413 ext_description => [],
2415 $detail->{'ref'} = $line_item->{'pkgnum'};
2416 $detail->{'description'} = encode_entities($line_item->{'description'});
2417 if ( exists $line_item->{'ext_description'} ) {
2418 @{$detail->{'ext_description'}} = map {
2419 encode_entities($_);
2420 } @{$line_item->{'ext_description'}};
2422 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2423 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2425 push @{$invoice_data{'detail_items'}}, $detail;
2430 foreach my $tax ( $self->_items_tax ) {
2432 $total->{'total_item'} = encode_entities($tax->{'description'});
2433 $taxtotal += $tax->{'amount'};
2434 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2435 push @{$invoice_data{'total_items'}}, $total;
2440 $total->{'total_item'} = 'Sub-total';
2441 $total->{'total_amount'} =
2442 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2443 unshift @{$invoice_data{'total_items'}}, $total;
2446 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2449 $total->{'total_item'} = '<b>Total</b>';
2450 $total->{'total_amount'} =
2451 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2452 push @{$invoice_data{'total_items'}}, $total;
2455 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2458 foreach my $credit ( $self->_items_credits ) {
2460 $total->{'total_item'} = encode_entities($credit->{'description'});
2462 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2463 push @{$invoice_data{'total_items'}}, $total;
2467 foreach my $payment ( $self->_items_payments ) {
2469 $total->{'total_item'} = encode_entities($payment->{'description'});
2471 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2472 push @{$invoice_data{'total_items'}}, $total;
2477 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2478 $total->{'total_amount'} =
2479 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2480 push @{$invoice_data{'total_items'}}, $total;
2483 $html_template->fill_in( HASH => \%invoice_data);
2486 # quick subroutine for print_latex
2488 # There are ten characters that LaTeX treats as special characters, which
2489 # means that they do not simply typeset themselves:
2490 # # $ % & ~ _ ^ \ { }
2492 # TeX ignores blanks following an escaped character; if you want a blank (as
2493 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2497 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2498 $value =~ s/([<>])/\$$1\$/g;
2502 #utility methods for print_*
2504 sub balance_due_msg {
2506 my $msg = 'Balance Due';
2507 return $msg unless $conf->exists('invoice_default_terms');
2508 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2509 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2510 } elsif ( $conf->config('invoice_default_terms') ) {
2511 $msg .= ' - '. $conf->config('invoice_default_terms');
2518 my @display = scalar(@_)
2520 : qw( _items_previous _items_pkg );
2521 #: qw( _items_pkg );
2522 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2524 foreach my $display ( @display ) {
2525 push @b, $self->$display(@_);
2530 sub _items_previous {
2532 my $cust_main = $self->cust_main;
2533 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2535 foreach ( @pr_cust_bill ) {
2537 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2538 ' ('. time2str('%x',$_->_date). ')',
2539 #'pkgpart' => 'N/A',
2541 'amount' => sprintf("%.2f", $_->owed),
2547 # 'description' => 'Previous Balance',
2548 # #'pkgpart' => 'N/A',
2549 # 'pkgnum' => 'N/A',
2550 # 'amount' => sprintf("%10.2f", $pr_total ),
2551 # 'ext_description' => [ map {
2552 # "Invoice ". $_->invnum.
2553 # " (". time2str("%x",$_->_date). ") ".
2554 # sprintf("%10.2f", $_->owed)
2555 # } @pr_cust_bill ],
2562 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2563 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2568 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2569 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2572 sub _items_cust_bill_pkg {
2574 my $cust_bill_pkg = shift;
2577 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2579 my $desc = $cust_bill_pkg->desc;
2581 if ( $cust_bill_pkg->pkgnum > 0 ) {
2583 if ( $cust_bill_pkg->setup != 0 ) {
2584 my $description = $desc;
2585 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2586 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2587 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2589 description => $description,
2590 #pkgpart => $part_pkg->pkgpart,
2591 pkgnum => $cust_bill_pkg->pkgnum,
2592 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2593 ext_description => \@d,
2597 if ( $cust_bill_pkg->recur != 0 ) {
2599 description => "$desc (" .
2600 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2601 time2str('%x', $cust_bill_pkg->edate). ')',
2602 #pkgpart => $part_pkg->pkgpart,
2603 pkgnum => $cust_bill_pkg->pkgnum,
2604 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2606 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2607 $cust_bill_pkg->sdate),
2608 $cust_bill_pkg->details,
2613 } else { #pkgnum tax or one-shot line item (??)
2615 if ( $cust_bill_pkg->setup != 0 ) {
2617 'description' => $desc,
2618 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2621 if ( $cust_bill_pkg->recur != 0 ) {
2623 'description' => "$desc (".
2624 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2625 time2str("%x", $cust_bill_pkg->edate). ')',
2626 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2638 sub _items_credits {
2643 foreach ( $self->cust_credited ) {
2645 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2647 my $reason = $_->cust_credit->reason;
2648 #my $reason = substr($_->cust_credit->reason,0,32);
2649 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2650 $reason = " ($reason) " if $reason;
2652 #'description' => 'Credit ref\#'. $_->crednum.
2653 # " (". time2str("%x",$_->cust_credit->_date) .")".
2655 'description' => 'Credit applied '.
2656 time2str("%x",$_->cust_credit->_date). $reason,
2657 'amount' => sprintf("%.2f",$_->amount),
2660 #foreach ( @cr_cust_credit ) {
2662 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2663 # $money_char. sprintf("%10.2f",$_->credited)
2671 sub _items_payments {
2675 #get & print payments
2676 foreach ( $self->cust_bill_pay ) {
2678 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2681 'description' => "Payment received ".
2682 time2str("%x",$_->cust_pay->_date ),
2683 'amount' => sprintf("%.2f", $_->amount )
2702 sub process_reprint {
2703 process_re_X('print', @_);
2710 sub process_reemail {
2711 process_re_X('email', @_);
2719 process_re_X('fax', @_);
2722 use Storable qw(thaw);
2726 my( $method, $job ) = ( shift, shift );
2727 warn "process_re_X $method for job $job\n" if $DEBUG;
2729 my $param = thaw(decode_base64(shift));
2730 warn Dumper($param) if $DEBUG;
2741 my($method, $job, %param ) = @_;
2742 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2744 warn "re_X $method for job $job with param:\n".
2745 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2748 #some false laziness w/search/cust_bill.html
2750 my $orderby = 'ORDER BY cust_bill._date';
2754 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2755 push @where, "cust_bill._date >= $1";
2757 if ( $param{'end'} =~ /^(\d+)$/ ) {
2758 push @where, "cust_bill._date < $1";
2760 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2761 push @where, "cust_main.agentnum = $1";
2765 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2766 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2767 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2768 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2770 push @where, "0 != $owed"
2773 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2776 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2778 my $addl_from = 'left join cust_main using ( custnum )';
2780 if ( $param{'newest_percust'} ) {
2781 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2782 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2783 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2786 my @cust_bill = qsearch( 'cust_bill',
2788 "$distinct cust_bill.*",
2794 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2795 foreach my $cust_bill ( @cust_bill ) {
2796 $cust_bill->$method();
2798 if ( $job ) { #progressbar foo
2800 if ( time - $min_sec > $last ) {
2801 my $error = $job->update_statustext(
2802 int( 100 * $num / scalar(@cust_bill) )
2804 die $error if $error;
2819 print_text formatting (and some logic :/) is in source, but needs to be
2820 slurped in from a file. Also number of lines ($=).
2824 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2825 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base