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;
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 (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
279 sub cust_bill_event {
281 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
284 =item num_cust_bill_event
286 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
290 sub num_cust_bill_event {
293 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
294 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
295 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
296 $sth->fetchrow_arrayref->[0];
301 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
305 #false laziness w/cust_pkg.pm
309 'table' => 'cust_event',
310 'addl_from' => 'JOIN part_event USING ( eventpart )',
311 'hashref' => { 'tablenum' => $self->invnum },
312 'extra_sql' => " AND eventtable = 'cust_bill' ",
318 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
322 #false laziness w/cust_pkg.pm
326 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
327 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
328 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
329 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
330 $sth->fetchrow_arrayref->[0];
335 Returns the customer (see L<FS::cust_main>) for this invoice.
341 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
344 =item cust_suspend_if_balance_over AMOUNT
346 Suspends the customer associated with this invoice if the total amount owed on
347 this invoice and all older invoices is greater than the specified amount.
349 Returns a list: an empty list on success or a list of errors.
353 sub cust_suspend_if_balance_over {
354 my( $self, $amount ) = ( shift, shift );
355 my $cust_main = $self->cust_main;
356 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
359 $cust_main->suspend(@_);
365 Depreciated. See the cust_credited method.
367 #Returns a list consisting of the total previous credited (see
368 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
369 #outstanding credits (FS::cust_credit objects).
375 croak "FS::cust_bill->cust_credit depreciated; see ".
376 "FS::cust_bill->cust_credit_bill";
379 #my @cust_credit = sort { $a->_date <=> $b->_date }
380 # grep { $_->credited != 0 && $_->_date < $self->_date }
381 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
383 #foreach (@cust_credit) { $total += $_->credited; }
384 #$total, @cust_credit;
389 Depreciated. See the cust_bill_pay method.
391 #Returns all payments (see L<FS::cust_pay>) for this invoice.
397 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
399 #sort { $a->_date <=> $b->_date }
400 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
406 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
412 sort { $a->_date <=> $b->_date }
413 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
418 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
424 sort { $a->_date <=> $b->_date }
425 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
431 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
438 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
440 foreach (@taxlines) { $total += $_->setup; }
446 Returns the amount owed (still outstanding) on this invoice, which is charged
447 minus all payment applications (see L<FS::cust_bill_pay>) and credit
448 applications (see L<FS::cust_credit_bill>).
454 my $balance = $self->charged;
455 $balance -= $_->amount foreach ( $self->cust_bill_pay );
456 $balance -= $_->amount foreach ( $self->cust_credited );
457 $balance = sprintf( "%.2f", $balance);
458 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
462 =item apply_payments_and_credits
466 sub apply_payments_and_credits {
469 local $SIG{HUP} = 'IGNORE';
470 local $SIG{INT} = 'IGNORE';
471 local $SIG{QUIT} = 'IGNORE';
472 local $SIG{TERM} = 'IGNORE';
473 local $SIG{TSTP} = 'IGNORE';
474 local $SIG{PIPE} = 'IGNORE';
476 my $oldAutoCommit = $FS::UID::AutoCommit;
477 local $FS::UID::AutoCommit = 0;
480 $self->select_for_update; #mutex
482 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
483 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
485 while ( $self->owed > 0 and ( @payments || @credits ) ) {
488 if ( @payments && @credits ) {
490 #decide which goes first by weight of top (unapplied) line item
492 my @open_lineitems = $self->open_cust_bill_pkg;
495 max( map { $_->part_pkg->pay_weight || 0 }
500 my $max_credit_weight =
501 max( map { $_->part_pkg->credit_weight || 0 }
507 #if both are the same... payments first? it has to be something
508 if ( $max_pay_weight >= $max_credit_weight ) {
514 } elsif ( @payments ) {
516 } elsif ( @credits ) {
519 die "guru meditation #12 and 35";
522 if ( $app eq 'pay' ) {
524 my $payment = shift @payments;
526 $app = new FS::cust_bill_pay {
527 'paynum' => $payment->paynum,
528 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
531 } elsif ( $app eq 'credit' ) {
533 my $credit = shift @credits;
535 $app = new FS::cust_credit_bill {
536 'crednum' => $credit->crednum,
537 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
541 die "guru meditation #12 and 35";
544 $app->invnum( $self->invnum );
546 my $error = $app->insert;
548 $dbh->rollback if $oldAutoCommit;
549 return "Error inserting ". $app->table. " record: $error";
551 die $error if $error;
555 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
560 =item generate_email PARAMHASH
562 PARAMHASH can contain the following:
566 =item from => sender address, required
568 =item tempate => alternate template name, optional
570 =item print_text => text attachment arrayref, optional
572 =item subject => email subject, optional
576 Returns an argument list to be passed to L<FS::Misc::send_email>.
587 my $me = '[FS::cust_bill::generate_email]';
590 'from' => $args{'from'},
591 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
594 if (ref($args{'to'}) eq 'ARRAY') {
595 $return{'to'} = $args{'to'};
597 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
598 $self->cust_main->invoicing_list
602 if ( $conf->exists('invoice_html') ) {
604 warn "$me creating HTML/text multipart message"
607 $return{'nobody'} = 1;
609 my $alternative = build MIME::Entity
610 'Type' => 'multipart/alternative',
611 'Encoding' => '7bit',
612 'Disposition' => 'inline'
616 if ( $conf->exists('invoice_email_pdf')
617 and scalar($conf->config('invoice_email_pdf_note')) ) {
619 warn "$me using 'invoice_email_pdf_note' in multipart message"
621 $data = [ map { $_ . "\n" }
622 $conf->config('invoice_email_pdf_note')
627 warn "$me not using 'invoice_email_pdf_note' in multipart message"
629 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
630 $data = $args{'print_text'};
632 $data = [ $self->print_text('', $args{'template'}) ];
637 $alternative->attach(
638 'Type' => 'text/plain',
639 #'Encoding' => 'quoted-printable',
640 'Encoding' => '7bit',
642 'Disposition' => 'inline',
645 $args{'from'} =~ /\@([\w\.\-]+)/;
646 my $from = $1 || 'example.com';
647 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
649 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
651 if ( defined($args{'template'}) && length($args{'template'})
652 && -e "$path/logo_". $args{'template'}. ".png"
655 $file = "$path/logo_". $args{'template'}. ".png";
657 $file = "$path/logo.png";
660 my $image = build MIME::Entity
661 'Type' => 'image/png',
662 'Encoding' => 'base64',
664 'Filename' => 'logo.png',
665 'Content-ID' => "<$content_id>",
668 $alternative->attach(
669 'Type' => 'text/html',
670 'Encoding' => 'quoted-printable',
671 'Data' => [ '<html>',
674 ' '. encode_entities($return{'subject'}),
677 ' <body bgcolor="#e8e8e8">',
678 $self->print_html('', $args{'template'}, $content_id),
682 'Disposition' => 'inline',
683 #'Filename' => 'invoice.pdf',
686 if ( $conf->exists('invoice_email_pdf') ) {
691 # multipart/alternative
697 my $related = build MIME::Entity 'Type' => 'multipart/related',
698 'Encoding' => '7bit';
700 #false laziness w/Misc::send_email
701 $related->head->replace('Content-type',
703 '; boundary="'. $related->head->multipart_boundary. '"'.
704 '; type=multipart/alternative'
707 $related->add_part($alternative);
709 $related->add_part($image);
711 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
713 $return{'mimeparts'} = [ $related, $pdf ];
717 #no other attachment:
719 # multipart/alternative
724 $return{'content-type'} = 'multipart/related';
725 $return{'mimeparts'} = [ $alternative, $image ];
726 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
727 #$return{'disposition'} = 'inline';
733 if ( $conf->exists('invoice_email_pdf') ) {
734 warn "$me creating PDF attachment"
737 #mime parts arguments a la MIME::Entity->build().
738 $return{'mimeparts'} = [
739 { $self->mimebuild_pdf('', $args{'template'}) }
743 if ( $conf->exists('invoice_email_pdf')
744 and scalar($conf->config('invoice_email_pdf_note')) ) {
746 warn "$me using 'invoice_email_pdf_note'"
748 $return{'body'} = [ map { $_ . "\n" }
749 $conf->config('invoice_email_pdf_note')
754 warn "$me not using 'invoice_email_pdf_note'"
756 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
757 $return{'body'} = $args{'print_text'};
759 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
772 Returns a list suitable for passing to MIME::Entity->build(), representing
773 this invoice as PDF attachment.
780 'Type' => 'application/pdf',
781 'Encoding' => 'base64',
782 'Data' => [ $self->print_pdf(@_) ],
783 'Disposition' => 'attachment',
784 'Filename' => 'invoice.pdf',
788 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
790 Sends this invoice to the destinations configured for this customer: sends
791 email, prints and/or faxes. See L<FS::cust_main_invoice>.
793 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
795 AGENTNUM, if specified, means that this invoice will only be sent for customers
796 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
797 single agent) or an arrayref of agentnums.
799 INVOICE_FROM, if specified, overrides the default email invoice From: address.
806 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
807 or die "invalid invoice number: " . $opt{invnum};
809 my @args = ( $opt{template}, $opt{agentnum} );
810 push @args, $opt{invoice_from}
811 if exists($opt{invoice_from}) && $opt{invoice_from};
813 my $error = $self->send( @args );
814 die $error if $error;
820 my $template = scalar(@_) ? shift : '';
821 if ( scalar(@_) && $_[0] ) {
822 my $agentnums = ref($_[0]) ? shift : [ shift ];
823 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
829 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
831 my @invoicing_list = $self->cust_main->invoicing_list;
833 $self->email($template, $invoice_from)
834 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
836 $self->print($template)
837 if grep { $_ eq 'POST' } @invoicing_list; #postal
839 $self->fax($template)
840 if grep { $_ eq 'FAX' } @invoicing_list; #fax
846 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
850 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
852 INVOICE_FROM, if specified, overrides the default email invoice From: address.
856 sub queueable_email {
859 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
860 or die "invalid invoice number: " . $opt{invnum};
862 my @args = ( $opt{template} );
863 push @args, $opt{invoice_from}
864 if exists($opt{invoice_from}) && $opt{invoice_from};
866 my $error = $self->email( @args );
867 die $error if $error;
873 my $template = scalar(@_) ? shift : '';
877 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
879 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
880 $self->cust_main->invoicing_list;
882 #better to notify this person than silence
883 @invoicing_list = ($invoice_from) unless @invoicing_list;
885 my $error = send_email(
886 $self->generate_email(
887 'from' => $invoice_from,
888 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
889 'template' => $template,
892 die "can't email invoice: $error\n" if $error;
893 #die "$error\n" if $error;
897 =item lpr_data [ TEMPLATENAME ]
899 Returns the postscript or plaintext for this invoice as an arrayref.
901 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
906 my( $self, $template) = @_;
907 $conf->exists('invoice_latex')
908 ? [ $self->print_ps('', $template) ]
909 : [ $self->print_text('', $template) ];
912 =item print [ TEMPLATENAME ]
916 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
922 my $template = scalar(@_) ? shift : '';
924 do_print $self->lpr_data($template);
927 =item fax [ TEMPLATENAME ]
931 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
937 my $template = scalar(@_) ? shift : '';
939 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
940 unless $conf->exists('invoice_latex');
942 my $dialstring = $self->cust_main->getfield('fax');
945 my $error = send_fax( 'docdata' => $self->lpr_data($template),
946 'dialstring' => $dialstring,
948 die $error if $error;
952 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
954 Like B<send>, but only sends the invoice if it is the newest open invoice for
964 grep { $_->owed > 0 }
965 qsearch('cust_bill', {
966 'custnum' => $self->custnum,
967 #'_date' => { op=>'>', value=>$self->_date },
968 'invnum' => { op=>'>', value=>$self->invnum },
975 =item send_csv OPTION => VALUE, ...
977 Sends invoice as a CSV data-file to a remote host with the specified protocol.
981 protocol - currently only "ftp"
987 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
988 and YYMMDDHHMMSS is a timestamp.
990 See L</print_csv> for a description of the output format.
995 my($self, %opt) = @_;
999 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1000 mkdir $spooldir, 0700 unless -d $spooldir;
1002 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1003 my $file = "$spooldir/$tracctnum.csv";
1005 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1007 open(CSV, ">$file") or die "can't open $file: $!";
1015 if ( $opt{protocol} eq 'ftp' ) {
1016 eval "use Net::FTP;";
1018 $net = Net::FTP->new($opt{server}) or die @$;
1020 die "unknown protocol: $opt{protocol}";
1023 $net->login( $opt{username}, $opt{password} )
1024 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1026 $net->binary or die "can't set binary mode";
1028 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1030 $net->put($file) or die "can't put $file: $!";
1040 Spools CSV invoice data.
1046 =item format - 'default' or 'billco'
1048 =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>).
1050 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1052 =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.
1059 my($self, %opt) = @_;
1061 my $cust_main = $self->cust_main;
1063 if ( $opt{'dest'} ) {
1064 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1065 $cust_main->invoicing_list;
1066 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1067 || ! keys %invoicing_list;
1070 if ( $opt{'balanceover'} ) {
1072 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1075 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1076 mkdir $spooldir, 0700 unless -d $spooldir;
1078 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1082 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1083 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1086 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1088 open(CSV, ">>$file") or die "can't open $file: $!";
1089 flock(CSV, LOCK_EX);
1094 if ( lc($opt{'format'}) eq 'billco' ) {
1096 flock(CSV, LOCK_UN);
1101 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1104 open(CSV,">>$file") or die "can't open $file: $!";
1105 flock(CSV, LOCK_EX);
1111 flock(CSV, LOCK_UN);
1118 =item print_csv OPTION => VALUE, ...
1120 Returns CSV data for this invoice.
1124 format - 'default' or 'billco'
1126 Returns a list consisting of two scalars. The first is a single line of CSV
1127 header information for this invoice. The second is one or more lines of CSV
1128 detail information for this invoice.
1130 If I<format> is not specified or "default", the fields of the CSV file are as
1133 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1137 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1139 B<record_type> is C<cust_bill> for the initial header line only. The
1140 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1141 fields are filled in.
1143 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1144 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1147 =item invnum - invoice number
1149 =item custnum - customer number
1151 =item _date - invoice date
1153 =item charged - total invoice amount
1155 =item first - customer first name
1157 =item last - customer first name
1159 =item company - company name
1161 =item address1 - address line 1
1163 =item address2 - address line 1
1173 =item pkg - line item description
1175 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1177 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1179 =item sdate - start date for recurring fee
1181 =item edate - end date for recurring fee
1185 If I<format> is "billco", the fields of the header CSV file are as follows:
1187 +-------------------------------------------------------------------+
1188 | FORMAT HEADER FILE |
1189 |-------------------------------------------------------------------|
1190 | Field | Description | Name | Type | Width |
1191 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1192 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1193 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1194 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1195 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1196 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1197 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1198 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1199 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1200 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1201 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1202 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1203 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1204 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1205 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1206 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1207 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1208 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1209 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1210 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1211 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1212 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1213 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1214 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1215 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1216 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1217 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1218 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1219 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1220 +-------+-------------------------------+------------+------+-------+
1222 If I<format> is "billco", the fields of the detail CSV file are as follows:
1224 FORMAT FOR DETAIL FILE
1226 Field | Description | Name | Type | Width
1227 1 | N/A-Leave Empty | RC | CHAR | 2
1228 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1229 3 | Account Number | TRACCTNUM | CHAR | 15
1230 4 | Invoice Number | TRINVOICE | CHAR | 15
1231 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1232 6 | Transaction Detail | DETAILS | CHAR | 100
1233 7 | Amount | AMT | NUM* | 9
1234 8 | Line Format Control** | LNCTRL | CHAR | 2
1235 9 | Grouping Code | GROUP | CHAR | 2
1236 10 | User Defined | ACCT CODE | CHAR | 15
1241 my($self, %opt) = @_;
1243 eval "use Text::CSV_XS";
1246 my $cust_main = $self->cust_main;
1248 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1250 if ( lc($opt{'format'}) eq 'billco' ) {
1253 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1255 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1257 my( $previous_balance, @unused ) = $self->previous; #previous balance
1259 my $pmt_cr_applied = 0;
1260 $pmt_cr_applied += $_->{'amount'}
1261 foreach ( $self->_items_payments, $self->_items_credits ) ;
1263 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1266 '', # 1 | N/A-Leave Empty CHAR 2
1267 '', # 2 | N/A-Leave Empty CHAR 15
1268 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1269 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1270 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1271 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1272 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1273 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1274 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1275 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1276 '', # 10 | Ancillary Billing Information CHAR 30
1277 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1278 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1281 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1284 $duedate, # 14 | Bill Due Date CHAR 10
1286 $previous_balance, # 15 | Previous Balance NUM* 9
1287 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1288 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1289 $totaldue, # 18 | Total Amt Due NUM* 9
1290 $totaldue, # 19 | Total Amt Due NUM* 9
1291 '', # 20 | 30 Day Aging NUM* 9
1292 '', # 21 | 60 Day Aging NUM* 9
1293 '', # 22 | 90 Day Aging NUM* 9
1294 'N', # 23 | Y/N CHAR 1
1295 '', # 24 | Remittance automation CHAR 100
1296 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1297 $self->custnum, # 26 | Customer Reference Number CHAR 15
1298 '0', # 27 | Federal Tax*** NUM* 9
1299 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1300 '0', # 29 | Other Taxes & Fees*** NUM* 9
1309 time2str("%x", $self->_date),
1310 sprintf("%.2f", $self->charged),
1311 ( map { $cust_main->getfield($_) }
1312 qw( first last company address1 address2 city state zip country ) ),
1314 ) or die "can't create csv";
1317 my $header = $csv->string. "\n";
1320 if ( lc($opt{'format'}) eq 'billco' ) {
1323 foreach my $item ( $self->_items_pkg ) {
1326 '', # 1 | N/A-Leave Empty CHAR 2
1327 '', # 2 | N/A-Leave Empty CHAR 15
1328 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1329 $self->invnum, # 4 | Invoice Number CHAR 15
1330 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1331 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1332 $item->{'amount'}, # 7 | Amount NUM* 9
1333 '', # 8 | Line Format Control** CHAR 2
1334 '', # 9 | Grouping Code CHAR 2
1335 '', # 10 | User Defined CHAR 15
1338 $detail .= $csv->string. "\n";
1344 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1346 my($pkg, $setup, $recur, $sdate, $edate);
1347 if ( $cust_bill_pkg->pkgnum ) {
1349 ($pkg, $setup, $recur, $sdate, $edate) = (
1350 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1351 ( $cust_bill_pkg->setup != 0
1352 ? sprintf("%.2f", $cust_bill_pkg->setup )
1354 ( $cust_bill_pkg->recur != 0
1355 ? sprintf("%.2f", $cust_bill_pkg->recur )
1357 ( $cust_bill_pkg->sdate
1358 ? time2str("%x", $cust_bill_pkg->sdate)
1360 ($cust_bill_pkg->edate
1361 ?time2str("%x", $cust_bill_pkg->edate)
1365 } else { #pkgnum tax
1366 next unless $cust_bill_pkg->setup != 0;
1367 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1368 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1370 ($pkg, $setup, $recur, $sdate, $edate) =
1371 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1377 ( map { '' } (1..11) ),
1378 ($pkg, $setup, $recur, $sdate, $edate)
1379 ) or die "can't create csv";
1381 $detail .= $csv->string. "\n";
1387 ( $header, $detail );
1393 Pays this invoice with a compliemntary payment. If there is an error,
1394 returns the error, otherwise returns false.
1400 my $cust_pay = new FS::cust_pay ( {
1401 'invnum' => $self->invnum,
1402 'paid' => $self->owed,
1405 'payinfo' => $self->cust_main->payinfo,
1413 Attempts to pay this invoice with a credit card payment via a
1414 Business::OnlinePayment realtime gateway. See
1415 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1416 for supported processors.
1422 $self->realtime_bop( 'CC', @_ );
1427 Attempts to pay this invoice with an electronic check (ACH) payment via a
1428 Business::OnlinePayment realtime gateway. See
1429 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1430 for supported processors.
1436 $self->realtime_bop( 'ECHECK', @_ );
1441 Attempts to pay this invoice with phone bill (LEC) payment via a
1442 Business::OnlinePayment realtime gateway. See
1443 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1444 for supported processors.
1450 $self->realtime_bop( 'LEC', @_ );
1454 my( $self, $method ) = @_;
1456 my $cust_main = $self->cust_main;
1457 my $balance = $cust_main->balance;
1458 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1459 $amount = sprintf("%.2f", $amount);
1460 return "not run (balance $balance)" unless $amount > 0;
1462 my $description = 'Internet Services';
1463 if ( $conf->exists('business-onlinepayment-description') ) {
1464 my $dtempl = $conf->config('business-onlinepayment-description');
1466 my $agent_obj = $cust_main->agent
1467 or die "can't retreive agent for $cust_main (agentnum ".
1468 $cust_main->agentnum. ")";
1469 my $agent = $agent_obj->agent;
1470 my $pkgs = join(', ',
1471 map { $_->cust_pkg->part_pkg->pkg }
1472 grep { $_->pkgnum } $self->cust_bill_pkg
1474 $description = eval qq("$dtempl");
1477 $cust_main->realtime_bop($method, $amount,
1478 'description' => $description,
1479 'invnum' => $self->invnum,
1484 =item batch_card OPTION => VALUE...
1486 Adds a payment for this invoice to the pending credit card batch (see
1487 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1488 runs the payment using a realtime gateway.
1493 my ($self, %options) = @_;
1494 my $cust_main = $self->cust_main;
1496 $options{invnum} = $self->invnum;
1498 $cust_main->batch_card(%options);
1501 sub _agent_template {
1503 $self->cust_main->agent_template;
1506 sub _agent_invoice_from {
1508 $self->cust_main->agent_invoice_from;
1511 =item print_text [ TIME [ , TEMPLATE ] ]
1513 Returns an text invoice, as a list of lines.
1515 TIME an optional value used to control the printing of overdue messages. The
1516 default is now. It isn't the date of the invoice; that's the `_date' field.
1517 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1518 L<Time::Local> and L<Date::Parse> for conversion functions.
1522 #still some false laziness w/_items stuff (and send_csv)
1525 my( $self, $today, $template ) = @_;
1528 # my $invnum = $self->invnum;
1529 my $cust_main = $self->cust_main;
1530 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1531 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1533 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1534 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1535 #my $balance_due = $self->owed + $pr_total - $cr_total;
1536 my $balance_due = $self->owed + $pr_total;
1539 #my($description,$amount);
1543 foreach ( @pr_cust_bill ) {
1545 "Previous Balance, Invoice #". $_->invnum.
1546 " (". time2str("%x",$_->_date). ")",
1547 $money_char. sprintf("%10.2f",$_->owed)
1550 if (@pr_cust_bill) {
1551 push @buf,['','-----------'];
1552 push @buf,[ 'Total Previous Balance',
1553 $money_char. sprintf("%10.2f",$pr_total ) ];
1558 foreach my $cust_bill_pkg (
1559 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1560 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1563 my $desc = $cust_bill_pkg->desc;
1565 if ( $cust_bill_pkg->pkgnum > 0 ) {
1567 if ( $cust_bill_pkg->setup != 0 ) {
1568 my $description = $desc;
1569 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1570 push @buf, [ $description,
1571 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1573 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1574 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1577 if ( $cust_bill_pkg->recur != 0 ) {
1580 ( $conf->exists('disable_line_item_date_ranges')
1582 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1583 time2str("%x", $cust_bill_pkg->edate) . ")"
1585 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1588 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1589 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1590 $cust_bill_pkg->sdate );
1593 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1595 } else { #pkgnum tax or one-shot line item
1597 if ( $cust_bill_pkg->setup != 0 ) {
1599 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1601 if ( $cust_bill_pkg->recur != 0 ) {
1602 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1603 . time2str("%x", $cust_bill_pkg->edate). ")",
1604 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1612 push @buf,['','-----------'];
1613 push @buf,['Total New Charges',
1614 $money_char. sprintf("%10.2f",$self->charged) ];
1617 push @buf,['','-----------'];
1618 push @buf,['Total Charges',
1619 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1623 foreach ( $self->cust_credited ) {
1625 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1627 my $reason = substr($_->cust_credit->reason,0,32);
1628 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1629 $reason = " ($reason) " if $reason;
1631 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1633 $money_char. sprintf("%10.2f",$_->amount)
1636 #foreach ( @cr_cust_credit ) {
1638 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1639 # $money_char. sprintf("%10.2f",$_->credited)
1643 #get & print payments
1644 foreach ( $self->cust_bill_pay ) {
1646 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1649 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1650 $money_char. sprintf("%10.2f",$_->amount )
1655 my $balance_due_msg = $self->balance_due_msg;
1657 push @buf,['','-----------'];
1658 push @buf,[$balance_due_msg, $money_char.
1659 sprintf("%10.2f", $balance_due ) ];
1661 #create the template
1662 $template ||= $self->_agent_template;
1663 my $templatefile = 'invoice_template';
1664 $templatefile .= "_$template" if length($template);
1665 my @invoice_template = $conf->config($templatefile)
1666 or die "cannot load config file $templatefile";
1669 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1670 /invoice_lines\((\d*)\)/;
1671 $invoice_lines += $1 || scalar(@buf);
1674 die "no invoice_lines() functions in template?" unless $wasfunc;
1675 my $invoice_template = new Text::Template (
1677 SOURCE => [ map "$_\n", @invoice_template ],
1678 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1679 $invoice_template->compile()
1680 or die "can't compile template: $Text::Template::ERROR";
1682 #setup template variables
1683 package FS::cust_bill::_template; #!
1684 use vars qw( $company_name $company_address
1685 $custnum $invnum $date $agent @address $overdue
1686 $page $total_pages @buf
1689 $custnum = $self->custnum;
1690 $invnum = $self->invnum;
1691 $date = $self->_date;
1692 $agent = $self->cust_main->agent->agent;
1695 if ( $FS::cust_bill::invoice_lines ) {
1697 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1699 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1704 #format address (variable for the template)
1706 @address = ( '', '', '', '', '', '' );
1707 package FS::cust_bill; #!
1708 $FS::cust_bill::_template::address[$l++] =
1709 $cust_main->payname.
1710 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1711 ? " (P.O. #". $cust_main->payinfo. ")"
1715 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1716 if $cust_main->company;
1717 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1718 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1719 if $cust_main->address2;
1720 $FS::cust_bill::_template::address[$l++] =
1721 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1723 my $countrydefault = $conf->config('countrydefault') || 'US';
1724 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1725 unless $cust_main->country eq $countrydefault;
1727 # #overdue? (variable for the template)
1728 # $FS::cust_bill::_template::overdue = (
1730 # && $today > $self->_date
1731 ## && $self->printed > 1
1732 # && $self->printed > 0
1735 $FS::cust_bill::_template::company_name = $conf->config('company_name');
1736 $FS::cust_bill::_template::company_address =
1737 join("\n", $conf->config('company_address') ). "\n";
1739 #and subroutine for the template
1740 sub FS::cust_bill::_template::invoice_lines {
1741 my $lines = shift || scalar(@buf);
1743 scalar(@buf) ? shift @buf : [ '', '' ];
1749 $FS::cust_bill::_template::page = 1;
1753 push @collect, split("\n",
1754 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1756 $FS::cust_bill::_template::page++;
1759 map "$_\n", @collect;
1763 =item print_latex [ TIME [ , TEMPLATE ] ]
1765 Internal method - returns a filename of a filled-in LaTeX template for this
1766 invoice (Note: add ".tex" to get the actual filename), and a filename of
1767 an associated logo (with the .eps extension included).
1769 See print_ps and print_pdf for methods that return PostScript and PDF output.
1771 TIME an optional value used to control the printing of overdue messages. The
1772 default is now. It isn't the date of the invoice; that's the `_date' field.
1773 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1774 L<Time::Local> and L<Date::Parse> for conversion functions.
1778 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1781 my( $self, $today, $template ) = @_;
1783 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1786 my $cust_main = $self->cust_main;
1787 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1788 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1790 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1791 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1792 #my $balance_due = $self->owed + $pr_total - $cr_total;
1793 my $balance_due = $self->owed + $pr_total;
1795 #create the template
1796 $template ||= $self->_agent_template;
1797 my $templatefile = 'invoice_latex';
1798 my $suffix = length($template) ? "_$template" : '';
1799 $templatefile .= $suffix;
1800 my @invoice_template = map "$_\n", $conf->config($templatefile)
1801 or die "cannot load config file $templatefile";
1803 my($format, $text_template);
1804 if ( grep { /^%%Detail/ } @invoice_template ) {
1805 #change this to a die when the old code is removed
1806 warn "old-style invoice template $templatefile; ".
1807 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1810 $format = 'Text::Template';
1811 $text_template = new Text::Template(
1813 SOURCE => \@invoice_template,
1814 DELIMITERS => [ '[@--', '--@]' ],
1817 $text_template->compile()
1818 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1822 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1824 $returnaddress = join("\n",
1825 $conf->config_orbase('invoice_latexreturnaddress', $template)
1828 } elsif ( grep /\S/, $conf->config('company_address') ) {
1831 join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
1832 $conf->config('company_address')
1837 my $warning = "Couldn't find a return address; ".
1838 "do you need to set the company_address configuration value?";
1840 $returnaddress = '~';
1841 #$returnaddress = $warning;
1845 my %invoice_data = (
1846 'company_name' => $conf->config('company_name'),
1847 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1848 'custnum' => $self->custnum,
1849 'invnum' => $self->invnum,
1850 'date' => time2str('%b %o, %Y', $self->_date),
1851 'today' => time2str('%b %o, %Y', $today),
1852 'agent' => _latex_escape($cust_main->agent->agent),
1853 'payname' => _latex_escape($cust_main->payname),
1854 'company' => _latex_escape($cust_main->company),
1855 'address1' => _latex_escape($cust_main->address1),
1856 'address2' => _latex_escape($cust_main->address2),
1857 'city' => _latex_escape($cust_main->city),
1858 'state' => _latex_escape($cust_main->state),
1859 'zip' => _latex_escape($cust_main->zip),
1860 'returnaddress' => $returnaddress,
1862 'terms' => $self->terms,
1863 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1864 # better hang on to conf_dir for a while
1865 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1868 my $countrydefault = $conf->config('countrydefault') || 'US';
1869 if ( $cust_main->country eq $countrydefault ) {
1870 $invoice_data{'country'} = '';
1872 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1875 #do variable substitution in notes, footer, smallfooter
1876 foreach my $include (qw( notes footer smallfooter )) {
1878 my $inc_tt = new Text::Template (
1880 SOURCE => [ map "$_\n",
1881 $conf->config_orbase("invoice_latex$include", $template )
1883 DELIMITERS => [ '[@--', '--@]' ],
1884 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1887 or die "can't compile template: $Text::Template::ERROR";
1889 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1891 $invoice_data{$include} =~ s/\n+$//;
1894 $invoice_data{'po_line'} =
1895 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1896 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1900 if ( $format eq 'old' ) {
1903 my @total_item = ();
1904 while ( @invoice_template ) {
1905 my $line = shift @invoice_template;
1907 if ( $line =~ /^%%Detail\s*$/ ) {
1909 while ( ( my $line_item_line = shift @invoice_template )
1910 !~ /^%%EndDetail\s*$/ ) {
1911 push @line_item, $line_item_line;
1913 foreach my $line_item ( $self->_items ) {
1914 #foreach my $line_item ( $self->_items_pkg ) {
1915 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1916 $invoice_data{'description'} =
1917 _latex_escape($line_item->{'description'});
1918 if ( exists $line_item->{'ext_description'} ) {
1919 $invoice_data{'description'} .=
1920 "\\tabularnewline\n~~".
1921 join( "\\tabularnewline\n~~",
1922 map _latex_escape($_), @{$line_item->{'ext_description'}}
1925 $invoice_data{'amount'} = $line_item->{'amount'};
1926 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1928 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1931 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1933 while ( ( my $total_item_line = shift @invoice_template )
1934 !~ /^%%EndTotalDetails\s*$/ ) {
1935 push @total_item, $total_item_line;
1938 my @total_fill = ();
1941 foreach my $tax ( $self->_items_tax ) {
1942 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1943 $taxtotal += $tax->{'amount'};
1944 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1946 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1951 $invoice_data{'total_item'} = 'Sub-total';
1952 $invoice_data{'total_amount'} =
1953 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1954 unshift @total_fill,
1955 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1959 $invoice_data{'total_item'} = '\textbf{Total}';
1960 $invoice_data{'total_amount'} =
1961 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1963 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1966 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1969 foreach my $credit ( $self->_items_credits ) {
1970 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1972 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1974 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1979 foreach my $payment ( $self->_items_payments ) {
1980 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1982 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1984 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1988 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1989 $invoice_data{'total_amount'} =
1990 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1992 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1995 push @filled_in, @total_fill;
1998 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1999 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2000 push @filled_in, $line;
2011 } elsif ( $format eq 'Text::Template' ) {
2013 my @detail_items = ();
2014 my @total_items = ();
2016 $invoice_data{'detail_items'} = \@detail_items;
2017 $invoice_data{'total_items'} = \@total_items;
2019 foreach my $line_item ( $self->_items ) {
2021 ext_description => [],
2023 $detail->{'ref'} = $line_item->{'pkgnum'};
2024 $detail->{'quantity'} = 1;
2025 $detail->{'description'} = _latex_escape($line_item->{'description'});
2026 if ( exists $line_item->{'ext_description'} ) {
2027 @{$detail->{'ext_description'}} = map {
2029 } @{$line_item->{'ext_description'}};
2031 $detail->{'amount'} = $line_item->{'amount'};
2032 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2034 push @detail_items, $detail;
2039 foreach my $tax ( $self->_items_tax ) {
2041 $total->{'total_item'} = _latex_escape($tax->{'description'});
2042 $taxtotal += $tax->{'amount'};
2043 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2044 push @total_items, $total;
2049 $total->{'total_item'} = 'Sub-total';
2050 $total->{'total_amount'} =
2051 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2052 unshift @total_items, $total;
2057 $total->{'total_item'} = '\textbf{Total}';
2058 $total->{'total_amount'} =
2059 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2060 push @total_items, $total;
2063 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2066 foreach my $credit ( $self->_items_credits ) {
2068 $total->{'total_item'} = _latex_escape($credit->{'description'});
2070 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2071 push @total_items, $total;
2075 foreach my $payment ( $self->_items_payments ) {
2077 $total->{'total_item'} = _latex_escape($payment->{'description'});
2079 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2080 push @total_items, $total;
2085 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2086 $total->{'total_amount'} =
2087 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2088 push @total_items, $total;
2092 die "guru meditation #54";
2095 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2096 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2100 ) or die "can't open temp file: $!\n";
2102 if ($template && $conf->exists("logo_${template}.eps")) {
2103 print $lh $conf->config_binary("logo_${template}.eps")
2104 or die "can't write temp file: $!\n";
2106 print $lh $conf->config_binary('logo.eps')
2107 or die "can't write temp file: $!\n";
2110 $invoice_data{'logo_file'} = $lh->filename;
2112 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2116 ) or die "can't open temp file: $!\n";
2117 if ( $format eq 'old' ) {
2118 print $fh join('', @filled_in );
2119 } elsif ( $format eq 'Text::Template' ) {
2120 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2122 die "guru meditation #32";
2126 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2127 return ($1, $invoice_data{'logo_file'});
2131 =item print_ps [ TIME [ , TEMPLATE ] ]
2133 Returns an postscript invoice, as a scalar.
2135 TIME an optional value used to control the printing of overdue messages. The
2136 default is now. It isn't the date of the invoice; that's the `_date' field.
2137 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2138 L<Time::Local> and L<Date::Parse> for conversion functions.
2145 my ($file, $lfile) = $self->print_latex(@_);
2146 my $ps = generate_ps($file);
2152 =item print_pdf [ TIME [ , TEMPLATE ] ]
2154 Returns an PDF invoice, as a scalar.
2156 TIME an optional value used to control the printing of overdue messages. The
2157 default is now. It isn't the date of the invoice; that's the `_date' field.
2158 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2159 L<Time::Local> and L<Date::Parse> for conversion functions.
2166 my ($file, $lfile) = $self->print_latex(@_);
2168 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2171 #system('pdflatex', "$file.tex");
2172 #system('pdflatex', "$file.tex");
2173 #! LaTeX Error: Unknown graphics extension: .eps.
2175 my $sfile = shell_quote $file;
2177 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2178 or die "pslatex $file.tex failed; see $file.log for details?\n";
2179 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2180 or die "pslatex $file.tex failed; see $file.log for details?\n";
2182 #system('dvipdf', "$file.dvi", "$file.pdf" );
2184 "dvips -q -t letter -f $sfile.dvi ".
2185 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2188 or die "dvips | gs failed: $!";
2190 open(PDF, "<$file.pdf")
2191 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2193 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2207 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2209 Returns an HTML invoice, as a scalar.
2211 TIME an optional value used to control the printing of overdue messages. The
2212 default is now. It isn't the date of the invoice; that's the `_date' field.
2213 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2214 L<Time::Local> and L<Date::Parse> for conversion functions.
2216 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2217 when emailing the invoice as part of a multipart/related MIME email.
2221 #some falze laziness w/print_text and print_latex (and send_csv)
2223 my( $self, $today, $template, $cid ) = @_;
2226 my $cust_main = $self->cust_main;
2227 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2228 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2230 $template ||= $self->_agent_template;
2231 my $templatefile = 'invoice_html';
2232 my $suffix = length($template) ? "_$template" : '';
2233 $templatefile .= $suffix;
2234 my @html_template = map "$_\n", $conf->config($templatefile)
2235 or die "cannot load config file $templatefile";
2237 my $html_template = new Text::Template(
2239 SOURCE => \@html_template,
2240 DELIMITERS => [ '<%=', '%>' ],
2243 $html_template->compile()
2244 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2246 my %invoice_data = (
2247 'company_name' => $conf->config('company_name'),
2248 'company_address' => join("\n", $conf->config('company_address') ). "\n",
2249 'custnum' => $self->custnum,
2250 'invnum' => $self->invnum,
2251 'date' => time2str('%b %o, %Y', $self->_date),
2252 'today' => time2str('%b %o, %Y', $today),
2253 'agent' => encode_entities($cust_main->agent->agent),
2254 'payname' => encode_entities($cust_main->payname),
2255 'company' => encode_entities($cust_main->company),
2256 'address1' => encode_entities($cust_main->address1),
2257 'address2' => encode_entities($cust_main->address2),
2258 'city' => encode_entities($cust_main->city),
2259 'state' => encode_entities($cust_main->state),
2260 'zip' => encode_entities($cust_main->zip),
2261 'terms' => $self->terms,
2263 'template' => $template,
2264 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2268 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2269 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2272 $invoice_data{'returnaddress'} =
2273 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2275 } elsif ( grep /\S/,
2276 $conf->config_orbase( 'invoice_latexreturnaddress', $template ) ) {
2278 $invoice_data{'returnaddress'} =
2281 s/\\\\\*?\s*$/<BR>/;
2282 s/\\hyphenation\{[\w\s\-]+\}//;
2285 $conf->config_orbase( 'invoice_latexreturnaddress',
2290 } elsif ( grep /\S/, $conf->config('company_address') ) {
2292 $invoice_data{'returnaddress'} =
2293 join("\n", $conf->config('company_address') );
2297 my $warning = "Couldn't find a return address; ".
2298 "do you need to set the company_address configuration value?";
2300 #$invoice_data{'returnaddress'} = $warning;
2304 my $countrydefault = $conf->config('countrydefault') || 'US';
2305 if ( $cust_main->country eq $countrydefault ) {
2306 $invoice_data{'country'} = '';
2308 $invoice_data{'country'} =
2309 encode_entities(code2country($cust_main->country));
2313 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2314 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2316 $invoice_data{'notes'} =
2317 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2319 $invoice_data{'notes'} =
2321 s/%%(.*)$/<!-- $1 -->/g;
2322 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2323 s/\\begin\{enumerate\}/<ol>/g;
2325 s/\\end\{enumerate\}/<\/ol>/g;
2326 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2331 $conf->config_orbase('invoice_latexnotes', $template)
2335 # #do variable substitutions in notes
2336 # $invoice_data{'notes'} =
2338 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2339 # $conf->config_orbase('invoice_latexnotes', $suffix)
2343 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2344 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2346 $invoice_data{'footer'} =
2347 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2349 $invoice_data{'footer'} =
2350 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2351 $conf->config_orbase('invoice_latexfooter', $template)
2355 $invoice_data{'po_line'} =
2356 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2357 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2360 my $money_char = $conf->config('money_char') || '$';
2362 foreach my $line_item ( $self->_items ) {
2364 ext_description => [],
2366 $detail->{'ref'} = $line_item->{'pkgnum'};
2367 $detail->{'description'} = encode_entities($line_item->{'description'});
2368 if ( exists $line_item->{'ext_description'} ) {
2369 @{$detail->{'ext_description'}} = map {
2370 encode_entities($_);
2371 } @{$line_item->{'ext_description'}};
2373 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2374 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2376 push @{$invoice_data{'detail_items'}}, $detail;
2381 foreach my $tax ( $self->_items_tax ) {
2383 $total->{'total_item'} = encode_entities($tax->{'description'});
2384 $taxtotal += $tax->{'amount'};
2385 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2386 push @{$invoice_data{'total_items'}}, $total;
2391 $total->{'total_item'} = 'Sub-total';
2392 $total->{'total_amount'} =
2393 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2394 unshift @{$invoice_data{'total_items'}}, $total;
2397 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2400 $total->{'total_item'} = '<b>Total</b>';
2401 $total->{'total_amount'} =
2402 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2403 push @{$invoice_data{'total_items'}}, $total;
2406 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2409 foreach my $credit ( $self->_items_credits ) {
2411 $total->{'total_item'} = encode_entities($credit->{'description'});
2413 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2414 push @{$invoice_data{'total_items'}}, $total;
2418 foreach my $payment ( $self->_items_payments ) {
2420 $total->{'total_item'} = encode_entities($payment->{'description'});
2422 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2423 push @{$invoice_data{'total_items'}}, $total;
2428 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2429 $total->{'total_amount'} =
2430 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2431 push @{$invoice_data{'total_items'}}, $total;
2434 $html_template->fill_in( HASH => \%invoice_data);
2437 # quick subroutine for print_latex
2439 # There are ten characters that LaTeX treats as special characters, which
2440 # means that they do not simply typeset themselves:
2441 # # $ % & ~ _ ^ \ { }
2443 # TeX ignores blanks following an escaped character; if you want a blank (as
2444 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2448 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2449 $value =~ s/([<>])/\$$1\$/g;
2453 #utility methods for print_*
2458 #check for an invoice- specific override (eventually)
2460 #check for a customer- specific override
2461 return $self->cust_main->invoice_terms
2462 if $self->cust_main->invoice_terms;
2464 #use configured default or default default
2465 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2471 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2472 $duedate = $self->_date() + ( $1 * 86400 );
2479 $self->due_date ? time2str(shift, $self->due_date) : '';
2482 sub balance_due_msg {
2484 my $msg = 'Balance Due';
2485 return $msg unless $self->terms;
2486 if ( $self->due_date ) {
2487 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2488 } elsif ( $self->terms ) {
2489 $msg .= ' - '. $self->terms;
2496 my @display = scalar(@_)
2498 : qw( _items_previous _items_pkg );
2499 #: qw( _items_pkg );
2500 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2502 foreach my $display ( @display ) {
2503 push @b, $self->$display(@_);
2508 sub _items_previous {
2510 my $cust_main = $self->cust_main;
2511 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2513 foreach ( @pr_cust_bill ) {
2515 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2516 ' ('. time2str('%x',$_->_date). ')',
2517 #'pkgpart' => 'N/A',
2519 'amount' => sprintf("%.2f", $_->owed),
2525 # 'description' => 'Previous Balance',
2526 # #'pkgpart' => 'N/A',
2527 # 'pkgnum' => 'N/A',
2528 # 'amount' => sprintf("%10.2f", $pr_total ),
2529 # 'ext_description' => [ map {
2530 # "Invoice ". $_->invnum.
2531 # " (". time2str("%x",$_->_date). ") ".
2532 # sprintf("%10.2f", $_->owed)
2533 # } @pr_cust_bill ],
2540 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2541 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2546 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2547 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2550 sub _items_cust_bill_pkg {
2552 my $cust_bill_pkg = shift;
2555 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2557 my $desc = $cust_bill_pkg->desc;
2559 if ( $cust_bill_pkg->pkgnum > 0 ) {
2561 if ( $cust_bill_pkg->setup != 0 ) {
2562 my $description = $desc;
2563 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2564 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2565 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2567 description => $description,
2568 #pkgpart => $part_pkg->pkgpart,
2569 pkgnum => $cust_bill_pkg->pkgnum,
2570 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2571 ext_description => \@d,
2575 if ( $cust_bill_pkg->recur != 0 ) {
2577 description => $desc .
2578 ( $conf->exists('disable_line_item_date_ranges')
2580 : " (" .time2str("%x", $cust_bill_pkg->sdate).
2581 " - ".time2str("%x", $cust_bill_pkg->edate).")"
2583 #pkgpart => $part_pkg->pkgpart,
2584 pkgnum => $cust_bill_pkg->pkgnum,
2585 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2587 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2588 $cust_bill_pkg->sdate),
2589 $cust_bill_pkg->details,
2594 } else { #pkgnum tax or one-shot line item (??)
2596 if ( $cust_bill_pkg->setup != 0 ) {
2598 'description' => $desc,
2599 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2602 if ( $cust_bill_pkg->recur != 0 ) {
2604 'description' => "$desc (".
2605 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2606 time2str("%x", $cust_bill_pkg->edate). ')',
2607 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2619 sub _items_credits {
2624 foreach ( $self->cust_credited ) {
2626 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2628 my $reason = $_->cust_credit->reason;
2629 #my $reason = substr($_->cust_credit->reason,0,32);
2630 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2631 $reason = " ($reason) " if $reason;
2633 #'description' => 'Credit ref\#'. $_->crednum.
2634 # " (". time2str("%x",$_->cust_credit->_date) .")".
2636 'description' => 'Credit applied '.
2637 time2str("%x",$_->cust_credit->_date). $reason,
2638 'amount' => sprintf("%.2f",$_->amount),
2641 #foreach ( @cr_cust_credit ) {
2643 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2644 # $money_char. sprintf("%10.2f",$_->credited)
2652 sub _items_payments {
2656 #get & print payments
2657 foreach ( $self->cust_bill_pay ) {
2659 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2662 'description' => "Payment received ".
2663 time2str("%x",$_->cust_pay->_date ),
2664 'amount' => sprintf("%.2f", $_->amount )
2683 sub process_reprint {
2684 process_re_X('print', @_);
2691 sub process_reemail {
2692 process_re_X('email', @_);
2700 process_re_X('fax', @_);
2703 use Storable qw(thaw);
2707 my( $method, $job ) = ( shift, shift );
2708 warn "process_re_X $method for job $job\n" if $DEBUG;
2710 my $param = thaw(decode_base64(shift));
2711 warn Dumper($param) if $DEBUG;
2722 my($method, $job, %param ) = @_;
2724 warn "re_X $method for job $job with param:\n".
2725 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2728 #some false laziness w/search/cust_bill.html
2730 my $orderby = 'ORDER BY cust_bill._date';
2734 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2735 push @where, "cust_bill._date >= $1";
2737 if ( $param{'end'} =~ /^(\d+)$/ ) {
2738 push @where, "cust_bill._date < $1";
2740 if ( $param{'invnum_min'} =~ /^(\d+)$/ ) {
2741 push @where, "cust_bill.invnum >= $1";
2743 if ( $param{'invnum_max'} =~ /^(\d+)$/ ) {
2744 push @where, "cust_bill.invnum <= $1";
2746 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2747 push @where, "cust_main.agentnum = $1";
2750 push @where, '0 != '. FS::cust_bill->owed_sql
2753 push @where, '0 != '. FS::cust_bill->net_sql
2756 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2759 if ( $param{'newest_percust'} ) {
2761 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2762 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2764 my @newest_where = map { s/\bcust_bill\./newest_cust_bill./g; }
2765 grep ! /^cust_main./, @where;
2766 my $newest_where = scalar(@newest_where)
2767 ? ' AND '. join(' AND ', @newest_where)
2770 push @where, "cust_bill._date = (
2771 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2772 WHERE newest_cust_bill.custnum = cust_bill.custnum
2778 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2780 my $addl_from = 'left join cust_main using ( custnum )';
2782 my @cust_bill = qsearch( 'cust_bill',
2784 #"$distinct cust_bill.*",
2791 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2792 foreach my $cust_bill ( @cust_bill ) {
2793 $cust_bill->$method();
2795 if ( $job ) { #progressbar foo
2797 if ( time - $min_sec > $last ) {
2798 my $error = $job->update_statustext(
2799 int( 100 * $num / scalar(@cust_bill) )
2801 die $error if $error;
2812 =head1 CLASS METHODS
2818 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2824 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2829 Returns an SQL fragment to retreive the net amount (charged minus credited).
2835 'charged - '. $class->credited_sql;
2840 Returns an SQL fragment to retreive the amount paid against this invoice.
2846 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2847 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2852 Returns an SQL fragment to retreive the amount credited against this invoice.
2858 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2859 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2870 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2871 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base