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 generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_statement;
20 use FS::cust_bill_pkg;
21 use FS::cust_bill_pkg_display;
25 use FS::cust_credit_bill;
27 use FS::cust_pay_batch;
28 use FS::cust_bill_event;
31 use FS::cust_bill_pay;
32 use FS::cust_bill_pay_batch;
33 use FS::part_bill_event;
36 @ISA = qw( FS::cust_main_Mixin FS::Record );
39 $me = '[FS::cust_bill]';
41 #ask FS::UID to run this stuff for us later
42 FS::UID->install_callback( sub {
44 $money_char = $conf->config('money_char') || '$';
49 FS::cust_bill - Object methods for cust_bill records
55 $record = new FS::cust_bill \%hash;
56 $record = new FS::cust_bill { 'column' => 'value' };
58 $error = $record->insert;
60 $error = $new_record->replace($old_record);
62 $error = $record->delete;
64 $error = $record->check;
66 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
68 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
70 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
72 @cust_pay_objects = $cust_bill->cust_pay;
74 $tax_amount = $record->tax;
76 @lines = $cust_bill->print_text;
77 @lines = $cust_bill->print_text $time;
81 An FS::cust_bill object represents an invoice; a declaration that a customer
82 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
83 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
84 following fields are currently supported:
90 =item invnum - primary key (assigned automatically for new invoices)
92 =item custnum - customer (see L<FS::cust_main>)
94 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
95 L<Time::Local> and L<Date::Parse> for conversion functions.
97 =item charged - amount of this invoice
105 =item printed - deprecated
113 =item closed - books closed flag, empty or `Y'
115 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
117 =item agent_invid - legacy invoice number
127 Creates a new invoice. To add the invoice to the database, see L<"insert">.
128 Invoices are normally created by calling the bill method of a customer object
129 (see L<FS::cust_main>).
133 sub table { 'cust_bill'; }
135 sub cust_linked { $_[0]->cust_main_custnum; }
136 sub cust_unlinked_msg {
138 "WARNING: can't find cust_main.custnum ". $self->custnum.
139 ' (cust_bill.invnum '. $self->invnum. ')';
144 Adds this invoice to the database ("Posts" the invoice). If there is an error,
145 returns the error, otherwise returns false.
149 This method now works but you probably shouldn't use it. Instead, apply a
150 credit against the invoice.
152 Using this method to delete invoices outright is really, really bad. There
153 would be no record you ever posted this invoice, and there are no check to
154 make sure charged = 0 or that there are no associated cust_bill_pkg records.
156 Really, don't use it.
162 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
164 local $SIG{HUP} = 'IGNORE';
165 local $SIG{INT} = 'IGNORE';
166 local $SIG{QUIT} = 'IGNORE';
167 local $SIG{TERM} = 'IGNORE';
168 local $SIG{TSTP} = 'IGNORE';
169 local $SIG{PIPE} = 'IGNORE';
171 my $oldAutoCommit = $FS::UID::AutoCommit;
172 local $FS::UID::AutoCommit = 0;
175 foreach my $table (qw(
187 foreach my $linked ( $self->$table() ) {
188 my $error = $linked->delete;
190 $dbh->rollback if $oldAutoCommit;
197 my $error = $self->SUPER::delete(@_);
199 $dbh->rollback if $oldAutoCommit;
203 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
209 =item replace OLD_RECORD
211 Replaces the OLD_RECORD with this one in the database. If there is an error,
212 returns the error, otherwise returns false.
214 Only printed may be changed. printed is normally updated by calling the
215 collect method of a customer object (see L<FS::cust_main>).
219 #replace can be inherited from Record.pm
221 # replace_check is now the preferred way to #implement replace data checks
222 # (so $object->replace() works without an argument)
225 my( $new, $old ) = ( shift, shift );
226 return "Can't change custnum!" unless $old->custnum == $new->custnum;
227 #return "Can't change _date!" unless $old->_date eq $new->_date;
228 return "Can't change _date!" unless $old->_date == $new->_date;
229 return "Can't change charged!" unless $old->charged == $new->charged
230 || $old->charged == 0;
237 Checks all fields to make sure this is a valid invoice. If there is an error,
238 returns the error, otherwise returns false. Called by the insert and replace
247 $self->ut_numbern('invnum')
248 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
249 || $self->ut_numbern('_date')
250 || $self->ut_money('charged')
251 || $self->ut_numbern('printed')
252 || $self->ut_enum('closed', [ '', 'Y' ])
253 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
254 || $self->ut_numbern('agent_invid') #varchar?
256 return $error if $error;
258 $self->_date(time) unless $self->_date;
260 $self->printed(0) if $self->printed eq '';
267 Returns the displayed invoice number for this invoice: agent_invid if
268 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
274 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
275 return $self->agent_invid;
277 return $self->invnum;
283 Returns a list consisting of the total previous balance for this customer,
284 followed by the previous outstanding invoices (as FS::cust_bill objects also).
291 my @cust_bill = sort { $a->_date <=> $b->_date }
292 grep { $_->owed != 0 && $_->_date < $self->_date }
293 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
295 foreach ( @cust_bill ) { $total += $_->owed; }
301 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
308 { 'table' => 'cust_bill_pkg',
309 'hashref' => { 'invnum' => $self->invnum },
310 'order_by' => 'ORDER BY billpkgnum',
315 =item cust_bill_pkg_pkgnum PKGNUM
317 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
322 sub cust_bill_pkg_pkgnum {
323 my( $self, $pkgnum ) = @_;
325 { 'table' => 'cust_bill_pkg',
326 'hashref' => { 'invnum' => $self->invnum,
329 'order_by' => 'ORDER BY billpkgnum',
336 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
343 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
345 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
348 =item open_cust_bill_pkg
350 Returns the open line items for this invoice.
352 Note that cust_bill_pkg with both setup and recur fees are returned as two
353 separate line items, each with only one fee.
357 # modeled after cust_main::open_cust_bill
358 sub open_cust_bill_pkg {
361 # grep { $_->owed > 0 } $self->cust_bill_pkg
363 my %other = ( 'recur' => 'setup',
364 'setup' => 'recur', );
366 foreach my $field ( qw( recur setup )) {
367 push @open, map { $_->set( $other{$field}, 0 ); $_; }
368 grep { $_->owed($field) > 0 }
369 $self->cust_bill_pkg;
375 =item cust_bill_event
377 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
381 sub cust_bill_event {
383 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
386 =item num_cust_bill_event
388 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
392 sub num_cust_bill_event {
395 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
396 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
397 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
398 $sth->fetchrow_arrayref->[0];
403 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
407 #false laziness w/cust_pkg.pm
411 'table' => 'cust_event',
412 'addl_from' => 'JOIN part_event USING ( eventpart )',
413 'hashref' => { 'tablenum' => $self->invnum },
414 'extra_sql' => " AND eventtable = 'cust_bill' ",
420 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
424 #false laziness w/cust_pkg.pm
428 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
429 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
430 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
431 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
432 $sth->fetchrow_arrayref->[0];
437 Returns the customer (see L<FS::cust_main>) for this invoice.
443 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
446 =item cust_suspend_if_balance_over AMOUNT
448 Suspends the customer associated with this invoice if the total amount owed on
449 this invoice and all older invoices is greater than the specified amount.
451 Returns a list: an empty list on success or a list of errors.
455 sub cust_suspend_if_balance_over {
456 my( $self, $amount ) = ( shift, shift );
457 my $cust_main = $self->cust_main;
458 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
461 $cust_main->suspend(@_);
467 Depreciated. See the cust_credited method.
469 #Returns a list consisting of the total previous credited (see
470 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
471 #outstanding credits (FS::cust_credit objects).
477 croak "FS::cust_bill->cust_credit depreciated; see ".
478 "FS::cust_bill->cust_credit_bill";
481 #my @cust_credit = sort { $a->_date <=> $b->_date }
482 # grep { $_->credited != 0 && $_->_date < $self->_date }
483 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
485 #foreach (@cust_credit) { $total += $_->credited; }
486 #$total, @cust_credit;
491 Depreciated. See the cust_bill_pay method.
493 #Returns all payments (see L<FS::cust_pay>) for this invoice.
499 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
501 #sort { $a->_date <=> $b->_date }
502 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
508 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
511 sub cust_bill_pay_batch {
513 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
518 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
524 sort { $a->_date <=> $b->_date }
525 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
530 =item cust_credit_bill
532 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
538 sort { $a->_date <=> $b->_date }
539 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
543 sub cust_credit_bill {
544 shift->cust_credited(@_);
547 =item cust_bill_pay_pkgnum PKGNUM
549 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
550 with matching pkgnum.
554 sub cust_bill_pay_pkgnum {
555 my( $self, $pkgnum ) = @_;
556 sort { $a->_date <=> $b->_date }
557 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
563 =item cust_credited_pkgnum PKGNUM
565 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
566 with matching pkgnum.
570 sub cust_credited_pkgnum {
571 my( $self, $pkgnum ) = @_;
572 sort { $a->_date <=> $b->_date }
573 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
581 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
588 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
590 foreach (@taxlines) { $total += $_->setup; }
596 Returns the amount owed (still outstanding) on this invoice, which is charged
597 minus all payment applications (see L<FS::cust_bill_pay>) and credit
598 applications (see L<FS::cust_credit_bill>).
604 my $balance = $self->charged;
605 $balance -= $_->amount foreach ( $self->cust_bill_pay );
606 $balance -= $_->amount foreach ( $self->cust_credited );
607 $balance = sprintf( "%.2f", $balance);
608 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
613 my( $self, $pkgnum ) = @_;
615 #my $balance = $self->charged;
617 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
619 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
620 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
622 $balance = sprintf( "%.2f", $balance);
623 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
627 =item apply_payments_and_credits [ OPTION => VALUE ... ]
629 Applies unapplied payments and credits to this invoice.
631 A hash of optional arguments may be passed. Currently "manual" is supported.
632 If true, a payment receipt is sent instead of a statement when
633 'payment_receipt_email' configuration option is set.
635 If there is an error, returns the error, otherwise returns false.
639 sub apply_payments_and_credits {
640 my( $self, %options ) = @_;
642 local $SIG{HUP} = 'IGNORE';
643 local $SIG{INT} = 'IGNORE';
644 local $SIG{QUIT} = 'IGNORE';
645 local $SIG{TERM} = 'IGNORE';
646 local $SIG{TSTP} = 'IGNORE';
647 local $SIG{PIPE} = 'IGNORE';
649 my $oldAutoCommit = $FS::UID::AutoCommit;
650 local $FS::UID::AutoCommit = 0;
653 $self->select_for_update; #mutex
655 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
656 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
658 if ( $conf->exists('pkg-balances') ) {
659 # limit @payments & @credits to those w/ a pkgnum grepped from $self
660 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
661 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
662 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
665 while ( $self->owed > 0 and ( @payments || @credits ) ) {
668 if ( @payments && @credits ) {
670 #decide which goes first by weight of top (unapplied) line item
672 my @open_lineitems = $self->open_cust_bill_pkg;
675 max( map { $_->part_pkg->pay_weight || 0 }
680 my $max_credit_weight =
681 max( map { $_->part_pkg->credit_weight || 0 }
687 #if both are the same... payments first? it has to be something
688 if ( $max_pay_weight >= $max_credit_weight ) {
694 } elsif ( @payments ) {
696 } elsif ( @credits ) {
699 die "guru meditation #12 and 35";
703 if ( $app eq 'pay' ) {
705 my $payment = shift @payments;
706 $unapp_amount = $payment->unapplied;
707 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
708 $app->pkgnum( $payment->pkgnum )
709 if $conf->exists('pkg-balances') && $payment->pkgnum;
711 } elsif ( $app eq 'credit' ) {
713 my $credit = shift @credits;
714 $unapp_amount = $credit->credited;
715 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
716 $app->pkgnum( $credit->pkgnum )
717 if $conf->exists('pkg-balances') && $credit->pkgnum;
720 die "guru meditation #12 and 35";
724 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
725 warn "owed_pkgnum ". $app->pkgnum;
726 $owed = $self->owed_pkgnum($app->pkgnum);
730 next unless $owed > 0;
732 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
733 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
735 $app->invnum( $self->invnum );
737 my $error = $app->insert(%options);
739 $dbh->rollback if $oldAutoCommit;
740 return "Error inserting ". $app->table. " record: $error";
742 die $error if $error;
746 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
751 =item generate_email OPTION => VALUE ...
759 sender address, required
763 alternate template name, optional
767 text attachment arrayref, optional
771 email subject, optional
775 Returns an argument list to be passed to L<FS::Misc::send_email>.
786 my $me = '[FS::cust_bill::generate_email]';
789 'from' => $args{'from'},
790 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
793 my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
795 if (ref($args{'to'}) eq 'ARRAY') {
796 $return{'to'} = $args{'to'};
798 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
799 $self->cust_main->invoicing_list
803 if ( $conf->exists('invoice_html') ) {
805 warn "$me creating HTML/text multipart message"
808 $return{'nobody'} = 1;
810 my $alternative = build MIME::Entity
811 'Type' => 'multipart/alternative',
812 'Encoding' => '7bit',
813 'Disposition' => 'inline'
817 if ( $conf->exists('invoice_email_pdf')
818 and scalar($conf->config('invoice_email_pdf_note')) ) {
820 warn "$me using 'invoice_email_pdf_note' in multipart message"
822 $data = [ map { $_ . "\n" }
823 $conf->config('invoice_email_pdf_note')
828 warn "$me not using 'invoice_email_pdf_note' in multipart message"
830 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
831 $data = $args{'print_text'};
833 $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
838 $alternative->attach(
839 'Type' => 'text/plain',
840 #'Encoding' => 'quoted-printable',
841 'Encoding' => '7bit',
843 'Disposition' => 'inline',
846 $args{'from'} =~ /\@([\w\.\-]+)/;
847 my $from = $1 || 'example.com';
848 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
851 my $agentnum = $self->cust_main->agentnum;
852 if ( defined($args{'template'}) && length($args{'template'})
853 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
856 $logo = 'logo_'. $args{'template'}. '.png';
860 my $image_data = $conf->config_binary( $logo, $agentnum);
862 my $image = build MIME::Entity
863 'Type' => 'image/png',
864 'Encoding' => 'base64',
865 'Data' => $image_data,
866 'Filename' => 'logo.png',
867 'Content-ID' => "<$content_id>",
870 $alternative->attach(
871 'Type' => 'text/html',
872 'Encoding' => 'quoted-printable',
873 'Data' => [ '<html>',
876 ' '. encode_entities($return{'subject'}),
879 ' <body bgcolor="#e8e8e8">',
880 $self->print_html({ time => '',
881 template => $args{'template'},
888 'Disposition' => 'inline',
889 #'Filename' => 'invoice.pdf',
893 if ( $self->cust_main->email_csv_cdr ) {
895 push @otherparts, build MIME::Entity
896 'Type' => 'text/csv',
897 'Encoding' => '7bit',
898 'Data' => [ map { "$_\n" }
899 $self->call_details('prepend_billed_number' => 1)
901 'Disposition' => 'attachment',
902 'Filename' => 'usage-'. $self->invnum. '.csv',
907 if ( $conf->exists('invoice_email_pdf') ) {
912 # multipart/alternative
918 my $related = build MIME::Entity 'Type' => 'multipart/related',
919 'Encoding' => '7bit';
921 #false laziness w/Misc::send_email
922 $related->head->replace('Content-type',
924 '; boundary="'. $related->head->multipart_boundary. '"'.
925 '; type=multipart/alternative'
928 $related->add_part($alternative);
930 $related->add_part($image);
932 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
934 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
938 #no other attachment:
940 # multipart/alternative
945 $return{'content-type'} = 'multipart/related';
946 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
947 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
948 #$return{'disposition'} = 'inline';
954 if ( $conf->exists('invoice_email_pdf') ) {
955 warn "$me creating PDF attachment"
958 #mime parts arguments a la MIME::Entity->build().
959 $return{'mimeparts'} = [
960 { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
964 if ( $conf->exists('invoice_email_pdf')
965 and scalar($conf->config('invoice_email_pdf_note')) ) {
967 warn "$me using 'invoice_email_pdf_note'"
969 $return{'body'} = [ map { $_ . "\n" }
970 $conf->config('invoice_email_pdf_note')
975 warn "$me not using 'invoice_email_pdf_note'"
977 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
978 $return{'body'} = $args{'print_text'};
980 $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
993 Returns a list suitable for passing to MIME::Entity->build(), representing
994 this invoice as PDF attachment.
1001 'Type' => 'application/pdf',
1002 'Encoding' => 'base64',
1003 'Data' => [ $self->print_pdf(@_) ],
1004 'Disposition' => 'attachment',
1005 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1009 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1011 Sends this invoice to the destinations configured for this customer: sends
1012 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1014 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1016 AGENTNUM, if specified, means that this invoice will only be sent for customers
1017 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1018 single agent) or an arrayref of agentnums.
1020 INVOICE_FROM, if specified, overrides the default email invoice From: address.
1022 AMOUNT, if specified, only sends the invoice if the total amount owed on this
1023 invoice and all older invoices is greater than the specified amount.
1027 sub queueable_send {
1030 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1031 or die "invalid invoice number: " . $opt{invnum};
1033 my @args = ( $opt{template}, $opt{agentnum} );
1034 push @args, $opt{invoice_from}
1035 if exists($opt{invoice_from}) && $opt{invoice_from};
1037 my $error = $self->send( @args );
1038 die $error if $error;
1044 my $template = scalar(@_) ? shift : '';
1045 if ( scalar(@_) && $_[0] ) {
1046 my $agentnums = ref($_[0]) ? shift : [ shift ];
1047 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
1053 : ( $self->_agent_invoice_from || #XXX should go away
1054 $conf->config('invoice_from', $self->cust_main->agentnum )
1057 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
1060 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1062 my @invoicing_list = $self->cust_main->invoicing_list;
1064 #$self->email_invoice($template, $invoice_from)
1065 $self->email($template, $invoice_from)
1066 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1068 #$self->print_invoice($template)
1069 $self->print($template)
1070 if grep { $_ eq 'POST' } @invoicing_list; #postal
1072 $self->fax_invoice($template)
1073 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1079 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
1081 Emails this invoice.
1083 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1085 INVOICE_FROM, if specified, overrides the default email invoice From: address.
1089 sub queueable_email {
1092 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1093 or die "invalid invoice number: " . $opt{invnum};
1095 my @args = ( $opt{template} );
1096 push @args, $opt{invoice_from}
1097 if exists($opt{invoice_from}) && $opt{invoice_from};
1099 my $error = $self->email( @args );
1100 die $error if $error;
1104 #sub email_invoice {
1107 my $template = scalar(@_) ? shift : '';
1111 : ( $self->_agent_invoice_from || #XXX should go away
1112 $conf->config('invoice_from', $self->cust_main->agentnum )
1116 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1117 $self->cust_main->invoicing_list;
1119 #better to notify this person than silence
1120 @invoicing_list = ($invoice_from) unless @invoicing_list;
1122 my $subject = $self->email_subject($template);
1124 my $error = send_email(
1125 $self->generate_email(
1126 'from' => $invoice_from,
1127 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1128 'subject' => $subject,
1129 'template' => $template,
1132 die "can't email invoice: $error\n" if $error;
1133 #die "$error\n" if $error;
1140 #my $template = scalar(@_) ? shift : '';
1143 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1146 my $cust_main = $self->cust_main;
1147 my $name = $cust_main->name;
1148 my $name_short = $cust_main->name_short;
1149 my $invoice_number = $self->invnum;
1150 my $invoice_date = $self->_date_pretty;
1152 eval qq("$subject");
1155 =item lpr_data [ TEMPLATENAME ]
1157 Returns the postscript or plaintext for this invoice as an arrayref.
1159 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1164 my( $self, $template) = @_;
1165 $conf->exists('invoice_latex')
1166 ? [ $self->print_ps('', $template) ]
1167 : [ $self->print_text('', $template) ];
1170 =item print [ TEMPLATENAME ]
1172 Prints this invoice.
1174 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1178 #sub print_invoice {
1181 my $template = scalar(@_) ? shift : '';
1183 do_print $self->lpr_data($template);
1186 =item fax_invoice [ TEMPLATENAME ]
1190 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1196 my $template = scalar(@_) ? shift : '';
1198 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1199 unless $conf->exists('invoice_latex');
1201 my $dialstring = $self->cust_main->getfield('fax');
1204 my $error = send_fax( 'docdata' => $self->lpr_data($template),
1205 'dialstring' => $dialstring,
1207 die $error if $error;
1211 =item ftp_invoice [ TEMPLATENAME ]
1213 Sends this invoice data via FTP.
1215 TEMPLATENAME is unused?
1221 my $template = scalar(@_) ? shift : '';
1224 'protocol' => 'ftp',
1225 'server' => $conf->config('cust_bill-ftpserver'),
1226 'username' => $conf->config('cust_bill-ftpusername'),
1227 'password' => $conf->config('cust_bill-ftppassword'),
1228 'dir' => $conf->config('cust_bill-ftpdir'),
1229 'format' => $conf->config('cust_bill-ftpformat'),
1233 =item spool_invoice [ TEMPLATENAME ]
1235 Spools this invoice data (see L<FS::spool_csv>)
1237 TEMPLATENAME is unused?
1243 my $template = scalar(@_) ? shift : '';
1246 'format' => $conf->config('cust_bill-spoolformat'),
1247 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1251 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1253 Like B<send>, but only sends the invoice if it is the newest open invoice for
1258 sub send_if_newest {
1263 grep { $_->owed > 0 }
1264 qsearch('cust_bill', {
1265 'custnum' => $self->custnum,
1266 #'_date' => { op=>'>', value=>$self->_date },
1267 'invnum' => { op=>'>', value=>$self->invnum },
1274 =item send_csv OPTION => VALUE, ...
1276 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1280 protocol - currently only "ftp"
1286 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1287 and YYMMDDHHMMSS is a timestamp.
1289 See L</print_csv> for a description of the output format.
1294 my($self, %opt) = @_;
1298 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1299 mkdir $spooldir, 0700 unless -d $spooldir;
1301 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1302 my $file = "$spooldir/$tracctnum.csv";
1304 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1306 open(CSV, ">$file") or die "can't open $file: $!";
1314 if ( $opt{protocol} eq 'ftp' ) {
1315 eval "use Net::FTP;";
1317 $net = Net::FTP->new($opt{server}) or die @$;
1319 die "unknown protocol: $opt{protocol}";
1322 $net->login( $opt{username}, $opt{password} )
1323 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1325 $net->binary or die "can't set binary mode";
1327 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1329 $net->put($file) or die "can't put $file: $!";
1339 Spools CSV invoice data.
1345 =item format - 'default' or 'billco'
1347 =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>).
1349 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1351 =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.
1358 my($self, %opt) = @_;
1360 my $cust_main = $self->cust_main;
1362 if ( $opt{'dest'} ) {
1363 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1364 $cust_main->invoicing_list;
1365 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1366 || ! keys %invoicing_list;
1369 if ( $opt{'balanceover'} ) {
1371 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1374 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1375 mkdir $spooldir, 0700 unless -d $spooldir;
1377 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1381 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1382 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1385 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1387 open(CSV, ">>$file") or die "can't open $file: $!";
1388 flock(CSV, LOCK_EX);
1393 if ( lc($opt{'format'}) eq 'billco' ) {
1395 flock(CSV, LOCK_UN);
1400 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1403 open(CSV,">>$file") or die "can't open $file: $!";
1404 flock(CSV, LOCK_EX);
1410 flock(CSV, LOCK_UN);
1417 =item print_csv OPTION => VALUE, ...
1419 Returns CSV data for this invoice.
1423 format - 'default' or 'billco'
1425 Returns a list consisting of two scalars. The first is a single line of CSV
1426 header information for this invoice. The second is one or more lines of CSV
1427 detail information for this invoice.
1429 If I<format> is not specified or "default", the fields of the CSV file are as
1432 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1436 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1438 B<record_type> is C<cust_bill> for the initial header line only. The
1439 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1440 fields are filled in.
1442 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1443 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1446 =item invnum - invoice number
1448 =item custnum - customer number
1450 =item _date - invoice date
1452 =item charged - total invoice amount
1454 =item first - customer first name
1456 =item last - customer first name
1458 =item company - company name
1460 =item address1 - address line 1
1462 =item address2 - address line 1
1472 =item pkg - line item description
1474 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1476 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1478 =item sdate - start date for recurring fee
1480 =item edate - end date for recurring fee
1484 If I<format> is "billco", the fields of the header CSV file are as follows:
1486 +-------------------------------------------------------------------+
1487 | FORMAT HEADER FILE |
1488 |-------------------------------------------------------------------|
1489 | Field | Description | Name | Type | Width |
1490 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1491 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1492 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1493 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1494 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1495 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1496 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1497 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1498 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1499 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1500 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1501 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1502 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1503 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1504 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1505 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1506 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1507 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1508 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1509 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1510 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1511 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1512 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1513 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1514 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1515 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1516 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1517 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1518 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1519 +-------+-------------------------------+------------+------+-------+
1521 If I<format> is "billco", the fields of the detail CSV file are as follows:
1523 FORMAT FOR DETAIL FILE
1525 Field | Description | Name | Type | Width
1526 1 | N/A-Leave Empty | RC | CHAR | 2
1527 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1528 3 | Account Number | TRACCTNUM | CHAR | 15
1529 4 | Invoice Number | TRINVOICE | CHAR | 15
1530 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1531 6 | Transaction Detail | DETAILS | CHAR | 100
1532 7 | Amount | AMT | NUM* | 9
1533 8 | Line Format Control** | LNCTRL | CHAR | 2
1534 9 | Grouping Code | GROUP | CHAR | 2
1535 10 | User Defined | ACCT CODE | CHAR | 15
1540 my($self, %opt) = @_;
1542 eval "use Text::CSV_XS";
1545 my $cust_main = $self->cust_main;
1547 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1549 if ( lc($opt{'format'}) eq 'billco' ) {
1552 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1554 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1556 my( $previous_balance, @unused ) = $self->previous; #previous balance
1558 my $pmt_cr_applied = 0;
1559 $pmt_cr_applied += $_->{'amount'}
1560 foreach ( $self->_items_payments, $self->_items_credits ) ;
1562 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1565 '', # 1 | N/A-Leave Empty CHAR 2
1566 '', # 2 | N/A-Leave Empty CHAR 15
1567 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1568 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1569 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1570 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1571 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1572 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1573 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1574 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1575 '', # 10 | Ancillary Billing Information CHAR 30
1576 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1577 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1580 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1583 $duedate, # 14 | Bill Due Date CHAR 10
1585 $previous_balance, # 15 | Previous Balance NUM* 9
1586 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1587 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1588 $totaldue, # 18 | Total Amt Due NUM* 9
1589 $totaldue, # 19 | Total Amt Due NUM* 9
1590 '', # 20 | 30 Day Aging NUM* 9
1591 '', # 21 | 60 Day Aging NUM* 9
1592 '', # 22 | 90 Day Aging NUM* 9
1593 'N', # 23 | Y/N CHAR 1
1594 '', # 24 | Remittance automation CHAR 100
1595 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1596 $self->custnum, # 26 | Customer Reference Number CHAR 15
1597 '0', # 27 | Federal Tax*** NUM* 9
1598 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1599 '0', # 29 | Other Taxes & Fees*** NUM* 9
1608 time2str("%x", $self->_date),
1609 sprintf("%.2f", $self->charged),
1610 ( map { $cust_main->getfield($_) }
1611 qw( first last company address1 address2 city state zip country ) ),
1613 ) or die "can't create csv";
1616 my $header = $csv->string. "\n";
1619 if ( lc($opt{'format'}) eq 'billco' ) {
1622 foreach my $item ( $self->_items_pkg ) {
1625 '', # 1 | N/A-Leave Empty CHAR 2
1626 '', # 2 | N/A-Leave Empty CHAR 15
1627 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1628 $self->invnum, # 4 | Invoice Number CHAR 15
1629 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1630 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1631 $item->{'amount'}, # 7 | Amount NUM* 9
1632 '', # 8 | Line Format Control** CHAR 2
1633 '', # 9 | Grouping Code CHAR 2
1634 '', # 10 | User Defined CHAR 15
1637 $detail .= $csv->string. "\n";
1643 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1645 my($pkg, $setup, $recur, $sdate, $edate);
1646 if ( $cust_bill_pkg->pkgnum ) {
1648 ($pkg, $setup, $recur, $sdate, $edate) = (
1649 $cust_bill_pkg->part_pkg->pkg,
1650 ( $cust_bill_pkg->setup != 0
1651 ? sprintf("%.2f", $cust_bill_pkg->setup )
1653 ( $cust_bill_pkg->recur != 0
1654 ? sprintf("%.2f", $cust_bill_pkg->recur )
1656 ( $cust_bill_pkg->sdate
1657 ? time2str("%x", $cust_bill_pkg->sdate)
1659 ($cust_bill_pkg->edate
1660 ?time2str("%x", $cust_bill_pkg->edate)
1664 } else { #pkgnum tax
1665 next unless $cust_bill_pkg->setup != 0;
1666 $pkg = $cust_bill_pkg->desc;
1667 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1668 ( $sdate, $edate ) = ( '', '' );
1674 ( map { '' } (1..11) ),
1675 ($pkg, $setup, $recur, $sdate, $edate)
1676 ) or die "can't create csv";
1678 $detail .= $csv->string. "\n";
1684 ( $header, $detail );
1690 Pays this invoice with a compliemntary payment. If there is an error,
1691 returns the error, otherwise returns false.
1697 my $cust_pay = new FS::cust_pay ( {
1698 'invnum' => $self->invnum,
1699 'paid' => $self->owed,
1702 'payinfo' => $self->cust_main->payinfo,
1710 Attempts to pay this invoice with a credit card payment via a
1711 Business::OnlinePayment realtime gateway. See
1712 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1713 for supported processors.
1719 $self->realtime_bop( 'CC', @_ );
1724 Attempts to pay this invoice with an electronic check (ACH) payment via a
1725 Business::OnlinePayment realtime gateway. See
1726 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1727 for supported processors.
1733 $self->realtime_bop( 'ECHECK', @_ );
1738 Attempts to pay this invoice with phone bill (LEC) payment via a
1739 Business::OnlinePayment realtime gateway. See
1740 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1741 for supported processors.
1747 $self->realtime_bop( 'LEC', @_ );
1751 my( $self, $method ) = @_;
1753 my $cust_main = $self->cust_main;
1754 my $balance = $cust_main->balance;
1755 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1756 $amount = sprintf("%.2f", $amount);
1757 return "not run (balance $balance)" unless $amount > 0;
1759 my $description = 'Internet Services';
1760 if ( $conf->exists('business-onlinepayment-description') ) {
1761 my $dtempl = $conf->config('business-onlinepayment-description');
1763 my $agent_obj = $cust_main->agent
1764 or die "can't retreive agent for $cust_main (agentnum ".
1765 $cust_main->agentnum. ")";
1766 my $agent = $agent_obj->agent;
1767 my $pkgs = join(', ',
1768 map { $_->part_pkg->pkg }
1769 grep { $_->pkgnum } $self->cust_bill_pkg
1771 $description = eval qq("$dtempl");
1774 $cust_main->realtime_bop($method, $amount,
1775 'description' => $description,
1776 'invnum' => $self->invnum,
1781 =item batch_card OPTION => VALUE...
1783 Adds a payment for this invoice to the pending credit card batch (see
1784 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1785 runs the payment using a realtime gateway.
1790 my ($self, %options) = @_;
1791 my $cust_main = $self->cust_main;
1793 $options{invnum} = $self->invnum;
1795 $cust_main->batch_card(%options);
1798 sub _agent_template {
1800 $self->cust_main->agent_template;
1803 sub _agent_invoice_from {
1805 $self->cust_main->agent_invoice_from;
1808 =item print_text [ TIME [ , TEMPLATE ] ]
1810 Returns an text invoice, as a list of lines.
1812 TIME an optional value used to control the printing of overdue messages. The
1813 default is now. It isn't the date of the invoice; that's the `_date' field.
1814 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1815 L<Time::Local> and L<Date::Parse> for conversion functions.
1820 my( $self, $today, $template, %opt ) = @_;
1822 my %params = ( 'format' => 'template' );
1823 $params{'time'} = $today if $today;
1824 $params{'template'} = $template if $template;
1825 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1827 $self->print_generic( %params );
1830 =item print_latex [ TIME [ , TEMPLATE ] ]
1832 Internal method - returns a filename of a filled-in LaTeX template for this
1833 invoice (Note: add ".tex" to get the actual filename), and a filename of
1834 an associated logo (with the .eps extension included).
1836 See print_ps and print_pdf for methods that return PostScript and PDF output.
1838 TIME an optional value used to control the printing of overdue messages. The
1839 default is now. It isn't the date of the invoice; that's the `_date' field.
1840 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1841 L<Time::Local> and L<Date::Parse> for conversion functions.
1846 my( $self, $today, $template, %opt ) = @_;
1848 my %params = ( 'format' => 'latex' );
1849 $params{'time'} = $today if $today;
1850 $params{'template'} = $template if $template;
1851 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1853 $template ||= $self->_agent_template;
1855 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1856 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1860 ) or die "can't open temp file: $!\n";
1862 my $agentnum = $self->cust_main->agentnum;
1864 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1865 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1866 or die "can't write temp file: $!\n";
1868 print $lh $conf->config_binary('logo.eps', $agentnum)
1869 or die "can't write temp file: $!\n";
1872 $params{'logo_file'} = $lh->filename;
1874 my @filled_in = $self->print_generic( %params );
1876 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1880 ) or die "can't open temp file: $!\n";
1881 print $fh join('', @filled_in );
1884 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1885 return ($1, $params{'logo_file'});
1889 =item print_generic OPTIONS_HASH
1891 Internal method - returns a filled-in template for this invoice as a scalar.
1893 See print_ps and print_pdf for methods that return PostScript and PDF output.
1895 Non optional options include
1896 format - latex, html, template
1898 Optional options include
1900 template - a value used as a suffix for a configuration template
1902 time - a value used to control the printing of overdue messages. The
1903 default is now. It isn't the date of the invoice; that's the `_date' field.
1904 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1905 L<Time::Local> and L<Date::Parse> for conversion functions.
1909 unsquelch_cdr - overrides any per customer cdr squelching when true
1913 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1914 # (alignment?) problems to change them all to '%.2f' ?
1917 my( $self, %params ) = @_;
1918 my $today = $params{today} ? $params{today} : time;
1919 warn "$me print_generic called on $self with suffix $params{template}\n"
1922 my $format = $params{format};
1923 die "Unknown format: $format"
1924 unless $format =~ /^(latex|html|template)$/;
1926 my $cust_main = $self->cust_main;
1927 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1928 unless $cust_main->payname
1929 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1931 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1932 'html' => [ '<%=', '%>' ],
1933 'template' => [ '{', '}' ],
1936 #create the template
1937 my $template = $params{template} ? $params{template} : $self->_agent_template;
1938 my $templatefile = "invoice_$format";
1939 $templatefile .= "_$template"
1940 if length($template);
1941 my @invoice_template = map "$_\n", $conf->config($templatefile)
1942 or die "cannot load config data $templatefile";
1945 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1946 #change this to a die when the old code is removed
1947 warn "old-style invoice template $templatefile; ".
1948 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1949 $old_latex = 'true';
1950 @invoice_template = _translate_old_latex_format(@invoice_template);
1953 my $text_template = new Text::Template(
1955 SOURCE => \@invoice_template,
1956 DELIMITERS => $delimiters{$format},
1959 $text_template->compile()
1960 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1963 # additional substitution could possibly cause breakage in existing templates
1964 my %convert_maps = (
1966 'notes' => sub { map "$_", @_ },
1967 'footer' => sub { map "$_", @_ },
1968 'smallfooter' => sub { map "$_", @_ },
1969 'returnaddress' => sub { map "$_", @_ },
1970 'coupon' => sub { map "$_", @_ },
1971 'summary' => sub { map "$_", @_ },
1977 s/%%(.*)$/<!-- $1 -->/g;
1978 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1979 s/\\begin\{enumerate\}/<ol>/g;
1981 s/\\end\{enumerate\}/<\/ol>/g;
1982 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1991 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1993 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1998 s/\\\\\*?\s*$/<BR>/;
1999 s/\\hyphenation\{[\w\s\-]+}//;
2004 'coupon' => sub { "" },
2005 'summary' => sub { "" },
2012 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2013 s/\\begin\{enumerate\}//g;
2015 s/\\end\{enumerate\}//g;
2016 s/\\textbf\{(.*)\}/$1/g;
2023 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2025 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2030 s/\\\\\*?\s*$/\n/; # dubious
2031 s/\\hyphenation\{[\w\s\-]+}//;
2035 'coupon' => sub { "" },
2036 'summary' => sub { "" },
2041 # hashes for differing output formats
2042 my %nbsps = ( 'latex' => '~',
2043 'html' => '', # '&nbps;' would be nice
2044 'template' => '', # not used
2046 my $nbsp = $nbsps{$format};
2048 my %escape_functions = ( 'latex' => \&_latex_escape,
2049 'html' => \&encode_entities,
2050 'template' => sub { shift },
2052 my $escape_function = $escape_functions{$format};
2054 my %date_formats = ( 'latex' => '%b %o, %Y',
2055 'html' => '%b %o, %Y',
2058 my $date_format = $date_formats{$format};
2060 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2062 'html' => sub { return '<b>'. shift(). '</b>'
2064 'template' => sub { shift },
2066 my $embolden_function = $embolden_functions{$format};
2069 # generate template variables
2072 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2076 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2082 $returnaddress = join("\n",
2083 $conf->config_orbase("invoice_${format}returnaddress", $template)
2086 } elsif ( grep /\S/,
2087 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2089 my $convert_map = $convert_maps{$format}{'returnaddress'};
2092 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2097 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2099 my $convert_map = $convert_maps{$format}{'returnaddress'};
2100 $returnaddress = join( "\n", &$convert_map(
2101 map { s/( {2,})/'~' x length($1)/eg;
2105 ( $conf->config('company_name', $self->cust_main->agentnum),
2106 $conf->config('company_address', $self->cust_main->agentnum),
2113 my $warning = "Couldn't find a return address; ".
2114 "do you need to set the company_address configuration value?";
2116 $returnaddress = $nbsp;
2117 #$returnaddress = $warning;
2121 my %invoice_data = (
2122 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2123 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2124 'custnum' => $cust_main->display_custnum,
2125 'invnum' => $self->invnum,
2126 'date' => time2str($date_format, $self->_date),
2127 'today' => time2str('%b %o, %Y', $today),
2128 'agent' => &$escape_function($cust_main->agent->agent),
2129 'agent_custid' => &$escape_function($cust_main->agent_custid),
2130 'payname' => &$escape_function($cust_main->payname),
2131 'company' => &$escape_function($cust_main->company),
2132 'address1' => &$escape_function($cust_main->address1),
2133 'address2' => &$escape_function($cust_main->address2),
2134 'city' => &$escape_function($cust_main->city),
2135 'state' => &$escape_function($cust_main->state),
2136 'zip' => &$escape_function($cust_main->zip),
2137 'fax' => &$escape_function($cust_main->fax),
2138 'returnaddress' => $returnaddress,
2140 'terms' => $self->terms,
2141 'template' => $template, #params{'template'},
2142 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
2143 # better hang on to conf_dir for a while
2144 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2147 'current_charges' => sprintf("%.2f", $self->charged),
2148 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2149 'ship_enable' => $conf->exists('invoice-ship_address'),
2150 'unitprices' => $conf->exists('invoice-unitprice'),
2153 $invoice_data{finance_section} = '';
2154 if ( $conf->config('finance_pkgclass') ) {
2156 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2157 $invoice_data{finance_section} = $pkg_class->categoryname;
2159 $invoice_data{finance_amount} = '0.00';
2161 my $countrydefault = $conf->config('countrydefault') || 'US';
2162 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2163 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2164 my $method = $prefix.$_;
2165 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2167 $invoice_data{'ship_country'} = ''
2168 if ( $invoice_data{'ship_country'} eq $countrydefault );
2170 $invoice_data{'cid'} = $params{'cid'}
2173 if ( $cust_main->country eq $countrydefault ) {
2174 $invoice_data{'country'} = '';
2176 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2180 $invoice_data{'address'} = \@address;
2182 $cust_main->payname.
2183 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2184 ? " (P.O. #". $cust_main->payinfo. ")"
2188 push @address, $cust_main->company
2189 if $cust_main->company;
2190 push @address, $cust_main->address1;
2191 push @address, $cust_main->address2
2192 if $cust_main->address2;
2194 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2195 push @address, $invoice_data{'country'}
2196 if $invoice_data{'country'};
2198 while (scalar(@address) < 5);
2200 $invoice_data{'logo_file'} = $params{'logo_file'}
2201 if $params{'logo_file'};
2203 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2204 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2205 #my $balance_due = $self->owed + $pr_total - $cr_total;
2206 my $balance_due = $self->owed + $pr_total;
2207 $invoice_data{'true_previous_balance'} = sprintf("%.2f", $self->previous_balance);
2208 $invoice_data{'balance_adjustments'} = sprintf("%.2f", $self->previous_balance - $self->billing_balance);
2209 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2210 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2212 my $agentnum = $self->cust_main->agentnum;
2214 my $summarypage = '';
2215 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2218 $invoice_data{'summarypage'} = $summarypage;
2220 #do variable substitution in notes, footer, smallfooter
2221 foreach my $include (qw( notes footer smallfooter coupon )) {
2223 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2226 if ( $conf->exists($inc_file, $agentnum)
2227 && length( $conf->config($inc_file, $agentnum) ) ) {
2229 @inc_src = $conf->config($inc_file, $agentnum);
2233 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2235 my $convert_map = $convert_maps{$format}{$include};
2237 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2238 s/--\@\]/$delimiters{$format}[1]/g;
2241 &$convert_map( $conf->config($inc_file, $agentnum) );
2245 my $inc_tt = new Text::Template (
2247 SOURCE => [ map "$_\n", @inc_src ],
2248 DELIMITERS => $delimiters{$format},
2249 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2251 unless ( $inc_tt->compile() ) {
2252 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2253 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2257 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2259 $invoice_data{$include} =~ s/\n+$//
2260 if ($format eq 'latex');
2263 $invoice_data{'po_line'} =
2264 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2265 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2268 my %money_chars = ( 'latex' => '',
2269 'html' => $conf->config('money_char') || '$',
2272 my $money_char = $money_chars{$format};
2274 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2275 'html' => $conf->config('money_char') || '$',
2278 my $other_money_char = $other_money_chars{$format};
2279 $invoice_data{'dollar'} = $other_money_char;
2281 my @detail_items = ();
2282 my @total_items = ();
2286 $invoice_data{'detail_items'} = \@detail_items;
2287 $invoice_data{'total_items'} = \@total_items;
2288 $invoice_data{'buf'} = \@buf;
2289 $invoice_data{'sections'} = \@sections;
2291 my $previous_section = { 'description' => 'Previous Charges',
2292 'subtotal' => $other_money_char.
2293 sprintf('%.2f', $pr_total),
2294 'summarized' => $summarypage ? 'Y' : '',
2298 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2299 'subtotal' => $taxtotal, # adjusted below
2300 'summarized' => $summarypage ? 'Y' : '',
2303 my $adjusttotal = 0;
2304 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2305 'subtotal' => 0, # adjusted below
2306 'summarized' => $summarypage ? 'Y' : '',
2309 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2310 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2311 my $late_sections = [];
2312 if ( $multisection ) {
2314 $self->_items_sections( $late_sections, $summarypage, $escape_function );
2316 push @sections, { 'description' => '', 'subtotal' => '' };
2319 unless ( $conf->exists('disable_previous_balance')
2320 || $conf->exists('previous_balance-summary_only')
2324 foreach my $line_item ( $self->_items_previous ) {
2327 ext_description => [],
2329 $detail->{'ref'} = $line_item->{'pkgnum'};
2330 $detail->{'quantity'} = 1;
2331 $detail->{'section'} = $previous_section;
2332 $detail->{'description'} = &$escape_function($line_item->{'description'});
2333 if ( exists $line_item->{'ext_description'} ) {
2334 @{$detail->{'ext_description'}} = map {
2335 &$escape_function($_);
2336 } @{$line_item->{'ext_description'}};
2338 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2339 $line_item->{'amount'};
2340 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2342 push @detail_items, $detail;
2343 push @buf, [ $detail->{'description'},
2344 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2350 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2351 push @buf, ['','-----------'];
2352 push @buf, [ 'Total Previous Balance',
2353 $money_char. sprintf("%10.2f", $pr_total) ];
2357 foreach my $section (@sections, @$late_sections) {
2359 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2360 if ( $invoice_data{finance_section} &&
2361 $section->{'description'} eq $invoice_data{finance_section} );
2363 $section->{'subtotal'} = $other_money_char.
2364 sprintf('%.2f', $section->{'subtotal'})
2367 if ( $section->{'description'} ) {
2368 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2374 $options{'section'} = $section if $multisection;
2375 $options{'format'} = $format;
2376 $options{'escape_function'} = $escape_function;
2377 $options{'format_function'} = sub { () } unless $unsquelched;
2378 $options{'unsquelched'} = $unsquelched;
2379 $options{'summary_page'} = $summarypage;
2381 foreach my $line_item ( $self->_items_pkg(%options) ) {
2383 ext_description => [],
2385 $detail->{'ref'} = $line_item->{'pkgnum'};
2386 $detail->{'quantity'} = $line_item->{'quantity'};
2387 $detail->{'section'} = $section;
2388 $detail->{'description'} = &$escape_function($line_item->{'description'});
2389 if ( exists $line_item->{'ext_description'} ) {
2390 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2392 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2393 $line_item->{'amount'};
2394 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2395 $line_item->{'unit_amount'};
2396 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2398 push @detail_items, $detail;
2399 push @buf, ( [ $detail->{'description'},
2400 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2402 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2406 if ( $section->{'description'} ) {
2407 push @buf, ( ['','-----------'],
2408 [ $section->{'description'}. ' sub-total',
2409 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2418 $invoice_data{current_less_finance} =
2419 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2421 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2422 unshift @sections, $previous_section if $pr_total;
2425 foreach my $tax ( $self->_items_tax ) {
2427 $taxtotal += $tax->{'amount'};
2429 my $description = &$escape_function( $tax->{'description'} );
2430 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2432 if ( $multisection ) {
2434 my $money = $old_latex ? '' : $money_char;
2435 push @detail_items, {
2436 ext_description => [],
2439 description => $description,
2440 amount => $money. $amount,
2442 section => $tax_section,
2447 push @total_items, {
2448 'total_item' => $description,
2449 'total_amount' => $other_money_char. $amount,
2454 push @buf,[ $description,
2455 $money_char. $amount,
2462 $total->{'total_item'} = 'Sub-total';
2463 $total->{'total_amount'} =
2464 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2466 if ( $multisection ) {
2467 $tax_section->{'subtotal'} = $other_money_char.
2468 sprintf('%.2f', $taxtotal);
2469 $tax_section->{'pretotal'} = 'New charges sub-total '.
2470 $total->{'total_amount'};
2471 push @sections, $tax_section if $taxtotal;
2473 unshift @total_items, $total;
2476 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2478 push @buf,['','-----------'];
2479 push @buf,[( $conf->exists('disable_previous_balance')
2481 : 'Total New Charges'
2483 $money_char. sprintf("%10.2f",$self->charged) ];
2488 $total->{'total_item'} = &$embolden_function('Total');
2489 $total->{'total_amount'} =
2490 &$embolden_function(
2493 $self->charged + ( $conf->exists('disable_previous_balance')
2499 if ( $multisection ) {
2500 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2501 sprintf('%.2f', $self->charged );
2503 push @total_items, $total;
2505 push @buf,['','-----------'];
2506 push @buf,['Total Charges',
2508 sprintf( '%10.2f', $self->charged +
2509 ( $conf->exists('disable_previous_balance')
2518 unless ( $conf->exists('disable_previous_balance') ) {
2519 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2522 my $credittotal = 0;
2523 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2526 $total->{'total_item'} = &$escape_function($credit->{'description'});
2527 $credittotal += $credit->{'amount'};
2528 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2529 $adjusttotal += $credit->{'amount'};
2530 if ( $multisection ) {
2531 my $money = $old_latex ? '' : $money_char;
2532 push @detail_items, {
2533 ext_description => [],
2536 description => &$escape_function($credit->{'description'}),
2537 amount => $money. $credit->{'amount'},
2539 section => $adjust_section,
2542 push @total_items, $total;
2546 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2549 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2550 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2554 my $paymenttotal = 0;
2555 foreach my $payment ( $self->_items_payments ) {
2557 $total->{'total_item'} = &$escape_function($payment->{'description'});
2558 $paymenttotal += $payment->{'amount'};
2559 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2560 $adjusttotal += $payment->{'amount'};
2561 if ( $multisection ) {
2562 my $money = $old_latex ? '' : $money_char;
2563 push @detail_items, {
2564 ext_description => [],
2567 description => &$escape_function($payment->{'description'}),
2568 amount => $money. $payment->{'amount'},
2570 section => $adjust_section,
2573 push @total_items, $total;
2575 push @buf, [ $payment->{'description'},
2576 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2579 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2581 if ( $multisection ) {
2582 $adjust_section->{'subtotal'} = $other_money_char.
2583 sprintf('%.2f', $adjusttotal);
2584 push @sections, $adjust_section;
2589 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2590 $total->{'total_amount'} =
2591 &$embolden_function(
2592 $other_money_char. sprintf('%.2f', $summarypage
2594 $self->billing_balance
2595 : $self->owed + $pr_total
2598 if ( $multisection ) {
2599 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2600 $total->{'total_amount'};
2602 push @total_items, $total;
2604 push @buf,['','-----------'];
2605 push @buf,[$self->balance_due_msg, $money_char.
2606 sprintf("%10.2f", $balance_due ) ];
2610 if ( $multisection ) {
2611 push @sections, @$late_sections
2615 my @includelist = ();
2616 push @includelist, 'summary' if $summarypage;
2617 foreach my $include ( @includelist ) {
2619 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2622 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2624 @inc_src = $conf->config($inc_file, $agentnum);
2628 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2630 my $convert_map = $convert_maps{$format}{$include};
2632 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2633 s/--\@\]/$delimiters{$format}[1]/g;
2636 &$convert_map( $conf->config($inc_file, $agentnum) );
2640 my $inc_tt = new Text::Template (
2642 SOURCE => [ map "$_\n", @inc_src ],
2643 DELIMITERS => $delimiters{$format},
2644 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2646 unless ( $inc_tt->compile() ) {
2647 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2648 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2652 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2654 $invoice_data{$include} =~ s/\n+$//
2655 if ($format eq 'latex');
2660 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2661 /invoice_lines\((\d*)\)/;
2662 $invoice_lines += $1 || scalar(@buf);
2665 die "no invoice_lines() functions in template?"
2666 if ( $format eq 'template' && !$wasfunc );
2668 if ($format eq 'template') {
2670 if ( $invoice_lines ) {
2671 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2672 $invoice_data{'total_pages'}++
2673 if scalar(@buf) % $invoice_lines;
2676 #setup subroutine for the template
2677 sub FS::cust_bill::_template::invoice_lines {
2678 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2680 scalar(@FS::cust_bill::_template::buf)
2681 ? shift @FS::cust_bill::_template::buf
2690 push @collect, split("\n",
2691 $text_template->fill_in( HASH => \%invoice_data,
2692 PACKAGE => 'FS::cust_bill::_template'
2695 $FS::cust_bill::_template::page++;
2697 map "$_\n", @collect;
2699 warn "filling in template for invoice ". $self->invnum. "\n"
2701 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2704 $text_template->fill_in(HASH => \%invoice_data);
2708 =item print_ps [ TIME [ , TEMPLATE ] ]
2710 Returns an postscript invoice, as a scalar.
2712 TIME an optional value used to control the printing of overdue messages. The
2713 default is now. It isn't the date of the invoice; that's the `_date' field.
2714 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2715 L<Time::Local> and L<Date::Parse> for conversion functions.
2722 my ($file, $lfile) = $self->print_latex(@_);
2723 my $ps = generate_ps($file);
2729 =item print_pdf [ TIME [ , TEMPLATE ] ]
2731 Returns an PDF invoice, as a scalar.
2733 TIME an optional value used to control the printing of overdue messages. The
2734 default is now. It isn't the date of the invoice; that's the `_date' field.
2735 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2736 L<Time::Local> and L<Date::Parse> for conversion functions.
2743 my ($file, $lfile) = $self->print_latex(@_);
2744 my $pdf = generate_pdf($file);
2750 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2752 Returns an HTML invoice, as a scalar.
2754 TIME an optional value used to control the printing of overdue messages. The
2755 default is now. It isn't the date of the invoice; that's the `_date' field.
2756 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2757 L<Time::Local> and L<Date::Parse> for conversion functions.
2759 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2760 when emailing the invoice as part of a multipart/related MIME email.
2768 %params = %{ shift() };
2770 $params{'time'} = shift;
2771 $params{'template'} = shift;
2772 $params{'cid'} = shift;
2775 $params{'format'} = 'html';
2777 $self->print_generic( %params );
2780 # quick subroutine for print_latex
2782 # There are ten characters that LaTeX treats as special characters, which
2783 # means that they do not simply typeset themselves:
2784 # # $ % & ~ _ ^ \ { }
2786 # TeX ignores blanks following an escaped character; if you want a blank (as
2787 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2791 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2792 $value =~ s/([<>])/\$$1\$/g;
2796 #utility methods for print_*
2798 sub _translate_old_latex_format {
2799 warn "_translate_old_latex_format called\n"
2806 if ( $line =~ /^%%Detail\s*$/ ) {
2808 push @template, q![@--!,
2809 q! foreach my $_tr_line (@detail_items) {!,
2810 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2811 q! $_tr_line->{'description'} .= !,
2812 q! "\\tabularnewline\n~~".!,
2813 q! join( "\\tabularnewline\n~~",!,
2814 q! @{$_tr_line->{'ext_description'}}!,
2818 while ( ( my $line_item_line = shift )
2819 !~ /^%%EndDetail\s*$/ ) {
2820 $line_item_line =~ s/'/\\'/g; # nice LTS
2821 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2822 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2823 push @template, " \$OUT .= '$line_item_line';";
2826 push @template, '}',
2829 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2831 push @template, '[@--',
2832 ' foreach my $_tr_line (@total_items) {';
2834 while ( ( my $total_item_line = shift )
2835 !~ /^%%EndTotalDetails\s*$/ ) {
2836 $total_item_line =~ s/'/\\'/g; # nice LTS
2837 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2838 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2839 push @template, " \$OUT .= '$total_item_line';";
2842 push @template, '}',
2846 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2847 push @template, $line;
2853 warn "$_\n" foreach @template;
2862 #check for an invoice- specific override (eventually)
2864 #check for a customer- specific override
2865 return $self->cust_main->invoice_terms
2866 if $self->cust_main->invoice_terms;
2868 #use configured default
2869 $conf->config('invoice_default_terms') || '';
2875 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2876 $duedate = $self->_date() + ( $1 * 86400 );
2883 $self->due_date ? time2str(shift, $self->due_date) : '';
2886 sub balance_due_msg {
2888 my $msg = 'Balance Due';
2889 return $msg unless $self->terms;
2890 if ( $self->due_date ) {
2891 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2892 } elsif ( $self->terms ) {
2893 $msg .= ' - '. $self->terms;
2898 sub balance_due_date {
2901 if ( $conf->exists('invoice_default_terms')
2902 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2903 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2908 =item invnum_date_pretty
2910 Returns a string with the invoice number and date, for example:
2911 "Invoice #54 (3/20/2008)"
2915 sub invnum_date_pretty {
2917 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2922 Returns a string with the date, for example: "3/20/2008"
2928 time2str('%x', $self->_date);
2931 sub _items_sections {
2934 my $summarypage = shift;
2941 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2945 my $usage = $cust_bill_pkg->usage;
2947 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2948 next if ( $display->summary && $summarypage );
2950 my $desc = $display->section;
2951 my $type = $display->type;
2953 if ( $cust_bill_pkg->pkgnum > 0 ) {
2954 $not_tax{$desc} = 1;
2957 if ( $display->post_total && !$summarypage ) {
2958 if (! $type || $type eq 'S') {
2959 $l{$desc} += $cust_bill_pkg->setup
2960 if ( $cust_bill_pkg->setup != 0 );
2964 $l{$desc} += $cust_bill_pkg->recur
2965 if ( $cust_bill_pkg->recur != 0 );
2968 if ($type && $type eq 'R') {
2969 $l{$desc} += $cust_bill_pkg->recur - $usage
2970 if ( $cust_bill_pkg->recur != 0 );
2973 if ($type && $type eq 'U') {
2974 $l{$desc} += $usage;
2978 if (! $type || $type eq 'S') {
2979 $s{$desc} += $cust_bill_pkg->setup
2980 if ( $cust_bill_pkg->setup != 0 );
2984 $s{$desc} += $cust_bill_pkg->recur
2985 if ( $cust_bill_pkg->recur != 0 );
2988 if ($type && $type eq 'R') {
2989 $s{$desc} += $cust_bill_pkg->recur - $usage
2990 if ( $cust_bill_pkg->recur != 0 );
2993 if ($type && $type eq 'U') {
2994 $s{$desc} += $usage;
3003 my %cache = map { $_->categoryname => $_ }
3004 qsearch( 'pkg_category', {disabled => 'Y'} );
3005 $cache{$_->categoryname} = $_
3006 foreach qsearch( 'pkg_category', {disabled => ''} );
3008 push @$late, map { { 'description' => &{$escape}($_),
3009 'subtotal' => $l{$_},
3012 sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
3014 map { { 'description' => &{$escape}($_),
3015 'subtotal' => $s{$_},
3016 'summarized' => $not_tax{$_} ? '' : 'Y',
3017 'tax_section' => $not_tax{$_} ? '' : 'Y',
3019 sort { $cache{$a}->weight <=> $cache{$b}->weight }
3021 ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
3030 #my @display = scalar(@_)
3032 # : qw( _items_previous _items_pkg );
3033 # #: qw( _items_pkg );
3034 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3035 my @display = qw( _items_previous _items_pkg );
3038 foreach my $display ( @display ) {
3039 push @b, $self->$display(@_);
3044 sub _items_previous {
3046 my $cust_main = $self->cust_main;
3047 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3049 foreach ( @pr_cust_bill ) {
3051 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3052 ' ('. time2str('%x',$_->_date). ')',
3053 #'pkgpart' => 'N/A',
3055 'amount' => sprintf("%.2f", $_->owed),
3061 # 'description' => 'Previous Balance',
3062 # #'pkgpart' => 'N/A',
3063 # 'pkgnum' => 'N/A',
3064 # 'amount' => sprintf("%10.2f", $pr_total ),
3065 # 'ext_description' => [ map {
3066 # "Invoice ". $_->invnum.
3067 # " (". time2str("%x",$_->_date). ") ".
3068 # sprintf("%10.2f", $_->owed)
3069 # } @pr_cust_bill ],
3076 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3077 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3081 return 0 unless $a cmp $b;
3082 return -1 if $b eq 'Tax';
3083 return 1 if $a eq 'Tax';
3084 return -1 if $b eq 'Other surcharges';
3085 return 1 if $a eq 'Other surcharges';
3091 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3092 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3095 sub _items_cust_bill_pkg {
3097 my $cust_bill_pkg = shift;
3100 my $format = $opt{format} || '';
3101 my $escape_function = $opt{escape_function} || sub { shift };
3102 my $format_function = $opt{format_function} || '';
3103 my $unsquelched = $opt{unsquelched} || '';
3104 my $section = $opt{section}->{description} if $opt{section};
3105 my $summary_page = $opt{summary_page} || '';
3108 my ($s, $r, $u) = ( undef, undef, undef );
3109 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3112 foreach ( $s, $r, $u ) {
3113 if ( $_ && !$cust_bill_pkg->hidden ) {
3114 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3115 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3121 foreach my $display ( grep { defined($section)
3122 ? $_->section eq $section
3125 grep { $_->summary || !$summary_page }
3126 $cust_bill_pkg->cust_bill_pkg_display
3130 my $type = $display->type;
3132 my $desc = $cust_bill_pkg->desc;
3133 $desc = substr($desc, 0, 50). '...'
3134 if $format eq 'latex' && length($desc) > 50;
3136 my %details_opt = ( 'format' => $format,
3137 'escape_function' => $escape_function,
3138 'format_function' => $format_function,
3141 if ( $cust_bill_pkg->pkgnum > 0 ) {
3143 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3145 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3147 my $description = $desc;
3148 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3151 push @d, map &{$escape_function}($_),
3152 $cust_pkg->h_labels_short($self->_date)
3153 unless $cust_pkg->part_pkg->hide_svc_detail
3154 || $cust_bill_pkg->hidden;
3155 push @d, $cust_bill_pkg->details(%details_opt)
3156 if $cust_bill_pkg->recur == 0;
3158 if ( $cust_bill_pkg->hidden ) {
3159 $s->{amount} += $cust_bill_pkg->setup;
3160 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3161 push @{ $s->{ext_description} }, @d;
3164 description => $description,
3165 #pkgpart => $part_pkg->pkgpart,
3166 pkgnum => $cust_bill_pkg->pkgnum,
3167 amount => $cust_bill_pkg->setup,
3168 unit_amount => $cust_bill_pkg->unitsetup,
3169 quantity => $cust_bill_pkg->quantity,
3170 ext_description => \@d,
3176 if ( $cust_bill_pkg->recur != 0 &&
3177 ( !$type || $type eq 'R' || $type eq 'U' )
3181 my $is_summary = $display->summary;
3182 my $description = $is_summary ? "Usage charges" : $desc;
3184 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3185 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3186 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3191 #at least until cust_bill_pkg has "past" ranges in addition to
3192 #the "future" sdate/edate ones... see #3032
3193 my @dates = ( $self->_date );
3194 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3195 push @dates, $prev->sdate if $prev;
3197 push @d, map &{$escape_function}($_),
3198 $cust_pkg->h_labels_short(@dates)
3199 #$cust_bill_pkg->edate,
3200 #$cust_bill_pkg->sdate)
3201 unless $cust_pkg->part_pkg->hide_svc_detail
3202 || $cust_bill_pkg->itemdesc
3203 || $cust_bill_pkg->hidden
3206 push @d, $cust_bill_pkg->details(%details_opt)
3207 unless ($is_summary || $type && $type eq 'R');
3211 $amount = $cust_bill_pkg->recur;
3212 }elsif($type eq 'R') {
3213 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3214 }elsif($type eq 'U') {
3215 $amount = $cust_bill_pkg->usage;
3218 if ( !$type || $type eq 'R' ) {
3220 if ( $cust_bill_pkg->hidden ) {
3221 $r->{amount} += $amount;
3222 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3223 push @{ $r->{ext_description} }, @d;
3226 description => $description,
3227 #pkgpart => $part_pkg->pkgpart,
3228 pkgnum => $cust_bill_pkg->pkgnum,
3230 unit_amount => $cust_bill_pkg->unitrecur,
3231 quantity => $cust_bill_pkg->quantity,
3232 ext_description => \@d,
3236 } elsif ( $amount ) { # && $type eq 'U'
3238 if ( $cust_bill_pkg->hidden ) {
3239 $u->{amount} += $amount;
3240 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3241 push @{ $u->{ext_description} }, @d;
3244 description => $description,
3245 #pkgpart => $part_pkg->pkgpart,
3246 pkgnum => $cust_bill_pkg->pkgnum,
3248 unit_amount => $cust_bill_pkg->unitrecur,
3249 quantity => $cust_bill_pkg->quantity,
3250 ext_description => \@d,
3256 } # recurring or usage with recurring charge
3258 } else { #pkgnum tax or one-shot line item (??)
3260 if ( $cust_bill_pkg->setup != 0 ) {
3262 'description' => $desc,
3263 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3266 if ( $cust_bill_pkg->recur != 0 ) {
3268 'description' => "$desc (".
3269 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3270 time2str("%x", $cust_bill_pkg->edate). ')',
3271 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3281 foreach ( $s, $r, $u ) {
3283 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3284 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3293 sub _items_credits {
3294 my( $self, %opt ) = @_;
3295 my $trim_len = $opt{'trim_len'} || 60;
3299 foreach ( $self->cust_credited ) {
3301 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3303 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3304 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3305 $reason = " ($reason) " if $reason;
3308 #'description' => 'Credit ref\#'. $_->crednum.
3309 # " (". time2str("%x",$_->cust_credit->_date) .")".
3311 'description' => 'Credit applied '.
3312 time2str("%x",$_->cust_credit->_date). $reason,
3313 'amount' => sprintf("%.2f",$_->amount),
3321 sub _items_payments {
3325 #get & print payments
3326 foreach ( $self->cust_bill_pay ) {
3328 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3331 'description' => "Payment received ".
3332 time2str("%x",$_->cust_pay->_date ),
3333 'amount' => sprintf("%.2f", $_->amount )
3341 =item call_details [ OPTION => VALUE ... ]
3343 Returns an array of CSV strings representing the call details for this invoice
3344 The only option available is the boolean prepend_billed_number
3349 my ($self, %opt) = @_;
3351 my $format_function = sub { shift };
3353 if ($opt{prepend_billed_number}) {
3354 $format_function = sub {
3358 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3363 my @details = map { $_->details( 'format_function' => $format_function,
3364 'escape_function' => sub{ return() },
3368 $self->cust_bill_pkg;
3369 my $header = $details[0];
3370 ( $header, grep { $_ ne $header } @details );
3380 =item process_reprint
3384 sub process_reprint {
3385 process_re_X('print', @_);
3388 =item process_reemail
3392 sub process_reemail {
3393 process_re_X('email', @_);
3401 process_re_X('fax', @_);
3409 process_re_X('ftp', @_);
3416 sub process_respool {
3417 process_re_X('spool', @_);
3420 use Storable qw(thaw);
3424 my( $method, $job ) = ( shift, shift );
3425 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3427 my $param = thaw(decode_base64(shift));
3428 warn Dumper($param) if $DEBUG;
3439 my($method, $job, %param ) = @_;
3441 warn "re_X $method for job $job with param:\n".
3442 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3445 #some false laziness w/search/cust_bill.html
3447 my $orderby = 'ORDER BY cust_bill._date';
3449 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3451 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3453 my @cust_bill = qsearch( {
3454 #'select' => "cust_bill.*",
3455 'table' => 'cust_bill',
3456 'addl_from' => $addl_from,
3458 'extra_sql' => $extra_sql,
3459 'order_by' => $orderby,
3463 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3465 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3468 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3469 foreach my $cust_bill ( @cust_bill ) {
3470 $cust_bill->$method();
3472 if ( $job ) { #progressbar foo
3474 if ( time - $min_sec > $last ) {
3475 my $error = $job->update_statustext(
3476 int( 100 * $num / scalar(@cust_bill) )
3478 die $error if $error;
3489 =head1 CLASS METHODS
3495 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3501 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3506 Returns an SQL fragment to retreive the net amount (charged minus credited).
3512 'charged - '. $class->credited_sql;
3517 Returns an SQL fragment to retreive the amount paid against this invoice.
3523 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3524 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3529 Returns an SQL fragment to retreive the amount credited against this invoice.
3535 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3536 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3539 =item search_sql HASHREF
3541 Class method which returns an SQL WHERE fragment to search for parameters
3542 specified in HASHREF. Valid parameters are
3548 Epoch date (UNIX timestamp) setting a lower bound for _date values
3552 Epoch date (UNIX timestamp) setting an upper bound for _date values
3566 =item newest_percust
3570 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3575 my($class, $param) = @_;
3577 warn "$me search_sql called with params: \n".
3578 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3583 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3584 push @search, "cust_bill._date >= $1";
3586 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3587 push @search, "cust_bill._date < $1";
3589 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3590 push @search, "cust_bill.invnum >= $1";
3592 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3593 push @search, "cust_bill.invnum <= $1";
3595 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3596 push @search, "cust_main.agentnum = $1";
3599 push @search, '0 != '. FS::cust_bill->owed_sql
3600 if $param->{'open'};
3602 push @search, '0 != '. FS::cust_bill->net_sql
3605 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3606 if $param->{'days'};
3608 if ( $param->{'newest_percust'} ) {
3610 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3611 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3613 my @newest_where = map { my $x = $_;
3614 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3617 grep ! /^cust_main./, @search;
3618 my $newest_where = scalar(@newest_where)
3619 ? ' AND '. join(' AND ', @newest_where)
3623 push @search, "cust_bill._date = (
3624 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3625 WHERE newest_cust_bill.custnum = cust_bill.custnum
3631 my $curuser = $FS::CurrentUser::CurrentUser;
3632 if ( $curuser->username eq 'fs_queue'
3633 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3635 my $newuser = qsearchs('access_user', {
3636 'username' => $username,
3640 $curuser = $newuser;
3642 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3646 push @search, $curuser->agentnums_sql;
3648 join(' AND ', @search );
3660 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3661 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base