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 "$_", @_ },
1976 s/%%(.*)$/<!-- $1 -->/g;
1977 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1978 s/\\begin\{enumerate\}/<ol>/g;
1980 s/\\end\{enumerate\}/<\/ol>/g;
1981 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1990 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1992 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1997 s/\\\\\*?\s*$/<BR>/;
1998 s/\\hyphenation\{[\w\s\-]+}//;
2003 'coupon' => sub { "" },
2010 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2011 s/\\begin\{enumerate\}//g;
2013 s/\\end\{enumerate\}//g;
2014 s/\\textbf\{(.*)\}/$1/g;
2021 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2023 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2028 s/\\\\\*?\s*$/\n/; # dubious
2029 s/\\hyphenation\{[\w\s\-]+}//;
2033 'coupon' => sub { "" },
2038 # hashes for differing output formats
2039 my %nbsps = ( 'latex' => '~',
2040 'html' => '', # '&nbps;' would be nice
2041 'template' => '', # not used
2043 my $nbsp = $nbsps{$format};
2045 my %escape_functions = ( 'latex' => \&_latex_escape,
2046 'html' => \&encode_entities,
2047 'template' => sub { shift },
2049 my $escape_function = $escape_functions{$format};
2051 my %date_formats = ( 'latex' => '%b %o, %Y',
2052 'html' => '%b %o, %Y',
2055 my $date_format = $date_formats{$format};
2057 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2059 'html' => sub { return '<b>'. shift(). '</b>'
2061 'template' => sub { shift },
2063 my $embolden_function = $embolden_functions{$format};
2066 # generate template variables
2069 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2073 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2079 $returnaddress = join("\n",
2080 $conf->config_orbase("invoice_${format}returnaddress", $template)
2083 } elsif ( grep /\S/,
2084 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2086 my $convert_map = $convert_maps{$format}{'returnaddress'};
2089 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2094 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2096 my $convert_map = $convert_maps{$format}{'returnaddress'};
2097 $returnaddress = join( "\n", &$convert_map(
2098 map { s/( {2,})/'~' x length($1)/eg;
2102 ( $conf->config('company_name', $self->cust_main->agentnum),
2103 $conf->config('company_address', $self->cust_main->agentnum),
2110 my $warning = "Couldn't find a return address; ".
2111 "do you need to set the company_address configuration value?";
2113 $returnaddress = $nbsp;
2114 #$returnaddress = $warning;
2118 my %invoice_data = (
2119 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2120 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2121 'custnum' => $cust_main->display_custnum,
2122 'invnum' => $self->invnum,
2123 'date' => time2str($date_format, $self->_date),
2124 'today' => time2str('%b %o, %Y', $today),
2125 'agent' => &$escape_function($cust_main->agent->agent),
2126 'agent_custid' => &$escape_function($cust_main->agent_custid),
2127 'payname' => &$escape_function($cust_main->payname),
2128 'company' => &$escape_function($cust_main->company),
2129 'address1' => &$escape_function($cust_main->address1),
2130 'address2' => &$escape_function($cust_main->address2),
2131 'city' => &$escape_function($cust_main->city),
2132 'state' => &$escape_function($cust_main->state),
2133 'zip' => &$escape_function($cust_main->zip),
2134 'fax' => &$escape_function($cust_main->fax),
2135 'returnaddress' => $returnaddress,
2137 'terms' => $self->terms,
2138 'template' => $template, #params{'template'},
2139 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
2140 # better hang on to conf_dir for a while
2141 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2144 'current_charges' => sprintf("%.2f", $self->charged),
2145 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2146 'ship_enable' => $conf->exists('invoice-ship_address'),
2147 'unitprices' => $conf->exists('invoice-unitprice'),
2150 my $countrydefault = $conf->config('countrydefault') || 'US';
2151 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2152 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2153 my $method = $prefix.$_;
2154 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2156 $invoice_data{'ship_country'} = ''
2157 if ( $invoice_data{'ship_country'} eq $countrydefault );
2159 $invoice_data{'cid'} = $params{'cid'}
2162 if ( $cust_main->country eq $countrydefault ) {
2163 $invoice_data{'country'} = '';
2165 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2169 $invoice_data{'address'} = \@address;
2171 $cust_main->payname.
2172 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2173 ? " (P.O. #". $cust_main->payinfo. ")"
2177 push @address, $cust_main->company
2178 if $cust_main->company;
2179 push @address, $cust_main->address1;
2180 push @address, $cust_main->address2
2181 if $cust_main->address2;
2183 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2184 push @address, $invoice_data{'country'}
2185 if $invoice_data{'country'};
2187 while (scalar(@address) < 5);
2189 $invoice_data{'logo_file'} = $params{'logo_file'}
2190 if $params{'logo_file'};
2192 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2193 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2194 #my $balance_due = $self->owed + $pr_total - $cr_total;
2195 my $balance_due = $self->owed + $pr_total;
2196 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2197 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2199 my $agentnum = $self->cust_main->agentnum;
2201 #do variable substitution in notes, footer, smallfooter
2202 foreach my $include (qw( notes footer smallfooter coupon )) {
2204 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2207 if ( $conf->exists($inc_file, $agentnum)
2208 && length( $conf->config($inc_file, $agentnum) ) ) {
2210 @inc_src = $conf->config($inc_file, $agentnum);
2214 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2216 my $convert_map = $convert_maps{$format}{$include};
2218 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2219 s/--\@\]/$delimiters{$format}[1]/g;
2222 &$convert_map( $conf->config($inc_file, $agentnum) );
2226 my $inc_tt = new Text::Template (
2228 SOURCE => [ map "$_\n", @inc_src ],
2229 DELIMITERS => $delimiters{$format},
2230 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2232 unless ( $inc_tt->compile() ) {
2233 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2234 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2238 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2240 $invoice_data{$include} =~ s/\n+$//
2241 if ($format eq 'latex');
2244 $invoice_data{'po_line'} =
2245 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2246 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2249 my %money_chars = ( 'latex' => '',
2250 'html' => $conf->config('money_char') || '$',
2253 my $money_char = $money_chars{$format};
2255 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2256 'html' => $conf->config('money_char') || '$',
2259 my $other_money_char = $other_money_chars{$format};
2261 my @detail_items = ();
2262 my @total_items = ();
2266 $invoice_data{'detail_items'} = \@detail_items;
2267 $invoice_data{'total_items'} = \@total_items;
2268 $invoice_data{'buf'} = \@buf;
2269 $invoice_data{'sections'} = \@sections;
2271 my $previous_section = { 'description' => 'Previous Charges',
2272 'subtotal' => $other_money_char.
2273 sprintf('%.2f', $pr_total),
2277 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2278 'subtotal' => $taxtotal }; # adjusted below
2280 my $adjusttotal = 0;
2281 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2282 'subtotal' => 0 }; # adjusted below
2284 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2285 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2286 my $late_sections = [];
2287 if ( $multisection ) {
2288 push @sections, $self->_items_sections( $late_sections );
2290 push @sections, { 'description' => '', 'subtotal' => '' };
2293 unless ( $conf->exists('disable_previous_balance')
2294 || $conf->exists('previous_balance-summary_only')
2298 foreach my $line_item ( $self->_items_previous ) {
2301 ext_description => [],
2303 $detail->{'ref'} = $line_item->{'pkgnum'};
2304 $detail->{'quantity'} = 1;
2305 $detail->{'section'} = $previous_section;
2306 $detail->{'description'} = &$escape_function($line_item->{'description'});
2307 if ( exists $line_item->{'ext_description'} ) {
2308 @{$detail->{'ext_description'}} = map {
2309 &$escape_function($_);
2310 } @{$line_item->{'ext_description'}};
2312 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2313 $line_item->{'amount'};
2314 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2316 push @detail_items, $detail;
2317 push @buf, [ $detail->{'description'},
2318 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2324 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2325 push @buf, ['','-----------'];
2326 push @buf, [ 'Total Previous Balance',
2327 $money_char. sprintf("%10.2f", $pr_total) ];
2331 foreach my $section (@sections, @$late_sections) {
2333 $section->{'subtotal'} = $other_money_char.
2334 sprintf('%.2f', $section->{'subtotal'})
2337 if ( $section->{'description'} ) {
2338 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2344 $options{'section'} = $section if $multisection;
2345 $options{'format'} = $format;
2346 $options{'escape_function'} = $escape_function;
2347 $options{'format_function'} = sub { () } unless $unsquelched;
2348 $options{'unsquelched'} = $unsquelched;
2350 foreach my $line_item ( $self->_items_pkg(%options) ) {
2352 ext_description => [],
2354 $detail->{'ref'} = $line_item->{'pkgnum'};
2355 $detail->{'quantity'} = $line_item->{'quantity'};
2356 $detail->{'section'} = $section;
2357 $detail->{'description'} = &$escape_function($line_item->{'description'});
2358 if ( exists $line_item->{'ext_description'} ) {
2359 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2361 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2362 $line_item->{'amount'};
2363 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2364 $line_item->{'unit_amount'};
2365 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2367 push @detail_items, $detail;
2368 push @buf, ( [ $detail->{'description'},
2369 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2371 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2375 if ( $section->{'description'} ) {
2376 push @buf, ( ['','-----------'],
2377 [ $section->{'description'}. ' sub-total',
2378 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2387 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2388 unshift @sections, $previous_section if $pr_total;
2391 foreach my $tax ( $self->_items_tax ) {
2393 $taxtotal += $tax->{'amount'};
2395 my $description = &$escape_function( $tax->{'description'} );
2396 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2398 if ( $multisection ) {
2400 my $money = $old_latex ? '' : $money_char;
2401 push @detail_items, {
2402 ext_description => [],
2405 description => $description,
2406 amount => $money. $amount,
2408 section => $tax_section,
2413 push @total_items, {
2414 'total_item' => $description,
2415 'total_amount' => $other_money_char. $amount,
2420 push @buf,[ $description,
2421 $money_char. $amount,
2428 $total->{'total_item'} = 'Sub-total';
2429 $total->{'total_amount'} =
2430 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2432 if ( $multisection ) {
2433 $tax_section->{'subtotal'} = $other_money_char.
2434 sprintf('%.2f', $taxtotal);
2435 $tax_section->{'pretotal'} = 'New charges sub-total '.
2436 $total->{'total_amount'};
2437 push @sections, $tax_section if $taxtotal;
2439 unshift @total_items, $total;
2442 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2444 push @buf,['','-----------'];
2445 push @buf,[( $conf->exists('disable_previous_balance')
2447 : 'Total New Charges'
2449 $money_char. sprintf("%10.2f",$self->charged) ];
2454 $total->{'total_item'} = &$embolden_function('Total');
2455 $total->{'total_amount'} =
2456 &$embolden_function(
2459 $self->charged + ( $conf->exists('disable_previous_balance')
2465 if ( $multisection ) {
2466 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2467 sprintf('%.2f', $self->charged );
2469 push @total_items, $total;
2471 push @buf,['','-----------'];
2472 push @buf,['Total Charges',
2474 sprintf( '%10.2f', $self->charged +
2475 ( $conf->exists('disable_previous_balance')
2484 unless ( $conf->exists('disable_previous_balance') ) {
2485 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2488 my $credittotal = 0;
2489 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2492 $total->{'total_item'} = &$escape_function($credit->{'description'});
2493 $credittotal += $credit->{'amount'};
2494 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2495 $adjusttotal += $credit->{'amount'};
2496 if ( $multisection ) {
2497 my $money = $old_latex ? '' : $money_char;
2498 push @detail_items, {
2499 ext_description => [],
2502 description => &$escape_function($credit->{'description'}),
2503 amount => $money. $credit->{'amount'},
2505 section => $adjust_section,
2508 push @total_items, $total;
2512 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2515 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2516 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2520 my $paymenttotal = 0;
2521 foreach my $payment ( $self->_items_payments ) {
2523 $total->{'total_item'} = &$escape_function($payment->{'description'});
2524 $paymenttotal += $payment->{'amount'};
2525 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2526 $adjusttotal += $payment->{'amount'};
2527 if ( $multisection ) {
2528 my $money = $old_latex ? '' : $money_char;
2529 push @detail_items, {
2530 ext_description => [],
2533 description => &$escape_function($payment->{'description'}),
2534 amount => $money. $payment->{'amount'},
2536 section => $adjust_section,
2539 push @total_items, $total;
2541 push @buf, [ $payment->{'description'},
2542 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2545 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2547 if ( $multisection ) {
2548 $adjust_section->{'subtotal'} = $other_money_char.
2549 sprintf('%.2f', $adjusttotal);
2550 push @sections, $adjust_section;
2555 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2556 $total->{'total_amount'} =
2557 &$embolden_function(
2558 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2560 if ( $multisection ) {
2561 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2562 $total->{'total_amount'};
2564 push @total_items, $total;
2566 push @buf,['','-----------'];
2567 push @buf,[$self->balance_due_msg, $money_char.
2568 sprintf("%10.2f", $balance_due ) ];
2572 if ( $multisection ) {
2573 push @sections, @$late_sections
2579 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2580 /invoice_lines\((\d*)\)/;
2581 $invoice_lines += $1 || scalar(@buf);
2584 die "no invoice_lines() functions in template?"
2585 if ( $format eq 'template' && !$wasfunc );
2587 if ($format eq 'template') {
2589 if ( $invoice_lines ) {
2590 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2591 $invoice_data{'total_pages'}++
2592 if scalar(@buf) % $invoice_lines;
2595 #setup subroutine for the template
2596 sub FS::cust_bill::_template::invoice_lines {
2597 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2599 scalar(@FS::cust_bill::_template::buf)
2600 ? shift @FS::cust_bill::_template::buf
2609 push @collect, split("\n",
2610 $text_template->fill_in( HASH => \%invoice_data,
2611 PACKAGE => 'FS::cust_bill::_template'
2614 $FS::cust_bill::_template::page++;
2616 map "$_\n", @collect;
2618 warn "filling in template for invoice ". $self->invnum. "\n"
2620 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2623 $text_template->fill_in(HASH => \%invoice_data);
2627 =item print_ps [ TIME [ , TEMPLATE ] ]
2629 Returns an postscript invoice, as a scalar.
2631 TIME an optional value used to control the printing of overdue messages. The
2632 default is now. It isn't the date of the invoice; that's the `_date' field.
2633 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2634 L<Time::Local> and L<Date::Parse> for conversion functions.
2641 my ($file, $lfile) = $self->print_latex(@_);
2642 my $ps = generate_ps($file);
2648 =item print_pdf [ TIME [ , TEMPLATE ] ]
2650 Returns an PDF invoice, as a scalar.
2652 TIME an optional value used to control the printing of overdue messages. The
2653 default is now. It isn't the date of the invoice; that's the `_date' field.
2654 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2655 L<Time::Local> and L<Date::Parse> for conversion functions.
2662 my ($file, $lfile) = $self->print_latex(@_);
2663 my $pdf = generate_pdf($file);
2669 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2671 Returns an HTML invoice, as a scalar.
2673 TIME an optional value used to control the printing of overdue messages. The
2674 default is now. It isn't the date of the invoice; that's the `_date' field.
2675 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2676 L<Time::Local> and L<Date::Parse> for conversion functions.
2678 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2679 when emailing the invoice as part of a multipart/related MIME email.
2687 %params = %{ shift() };
2689 $params{'time'} = shift;
2690 $params{'template'} = shift;
2691 $params{'cid'} = shift;
2694 $params{'format'} = 'html';
2696 $self->print_generic( %params );
2699 # quick subroutine for print_latex
2701 # There are ten characters that LaTeX treats as special characters, which
2702 # means that they do not simply typeset themselves:
2703 # # $ % & ~ _ ^ \ { }
2705 # TeX ignores blanks following an escaped character; if you want a blank (as
2706 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2710 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2711 $value =~ s/([<>])/\$$1\$/g;
2715 #utility methods for print_*
2717 sub _translate_old_latex_format {
2718 warn "_translate_old_latex_format called\n"
2725 if ( $line =~ /^%%Detail\s*$/ ) {
2727 push @template, q![@--!,
2728 q! foreach my $_tr_line (@detail_items) {!,
2729 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2730 q! $_tr_line->{'description'} .= !,
2731 q! "\\tabularnewline\n~~".!,
2732 q! join( "\\tabularnewline\n~~",!,
2733 q! @{$_tr_line->{'ext_description'}}!,
2737 while ( ( my $line_item_line = shift )
2738 !~ /^%%EndDetail\s*$/ ) {
2739 $line_item_line =~ s/'/\\'/g; # nice LTS
2740 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2741 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2742 push @template, " \$OUT .= '$line_item_line';";
2745 push @template, '}',
2748 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2750 push @template, '[@--',
2751 ' foreach my $_tr_line (@total_items) {';
2753 while ( ( my $total_item_line = shift )
2754 !~ /^%%EndTotalDetails\s*$/ ) {
2755 $total_item_line =~ s/'/\\'/g; # nice LTS
2756 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2757 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2758 push @template, " \$OUT .= '$total_item_line';";
2761 push @template, '}',
2765 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2766 push @template, $line;
2772 warn "$_\n" foreach @template;
2781 #check for an invoice- specific override (eventually)
2783 #check for a customer- specific override
2784 return $self->cust_main->invoice_terms
2785 if $self->cust_main->invoice_terms;
2787 #use configured default
2788 $conf->config('invoice_default_terms') || '';
2794 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2795 $duedate = $self->_date() + ( $1 * 86400 );
2802 $self->due_date ? time2str(shift, $self->due_date) : '';
2805 sub balance_due_msg {
2807 my $msg = 'Balance Due';
2808 return $msg unless $self->terms;
2809 if ( $self->due_date ) {
2810 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2811 } elsif ( $self->terms ) {
2812 $msg .= ' - '. $self->terms;
2817 sub balance_due_date {
2820 if ( $conf->exists('invoice_default_terms')
2821 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2822 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2827 =item invnum_date_pretty
2829 Returns a string with the invoice number and date, for example:
2830 "Invoice #54 (3/20/2008)"
2834 sub invnum_date_pretty {
2836 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2841 Returns a string with the date, for example: "3/20/2008"
2847 time2str('%x', $self->_date);
2850 sub _items_sections {
2857 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2860 if ( $cust_bill_pkg->pkgnum > 0 ) {
2861 my $usage = $cust_bill_pkg->usage;
2863 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2864 my $desc = $display->section;
2865 my $type = $display->type;
2867 if ( $display->post_total ) {
2868 if (! $type || $type eq 'S') {
2869 $l{$desc} += $cust_bill_pkg->setup
2870 if ( $cust_bill_pkg->setup != 0 );
2874 $l{$desc} += $cust_bill_pkg->recur
2875 if ( $cust_bill_pkg->recur != 0 );
2878 if ($type && $type eq 'R') {
2879 $l{$desc} += $cust_bill_pkg->recur - $usage
2880 if ( $cust_bill_pkg->recur != 0 );
2883 if ($type && $type eq 'U') {
2884 $l{$desc} += $usage;
2888 if (! $type || $type eq 'S') {
2889 $s{$desc} += $cust_bill_pkg->setup
2890 if ( $cust_bill_pkg->setup != 0 );
2894 $s{$desc} += $cust_bill_pkg->recur
2895 if ( $cust_bill_pkg->recur != 0 );
2898 if ($type && $type eq 'R') {
2899 $s{$desc} += $cust_bill_pkg->recur - $usage
2900 if ( $cust_bill_pkg->recur != 0 );
2903 if ($type && $type eq 'U') {
2904 $s{$desc} += $usage;
2915 push @$late, map { { 'description' => $_,
2916 'subtotal' => $l{$_},
2920 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2927 #my @display = scalar(@_)
2929 # : qw( _items_previous _items_pkg );
2930 # #: qw( _items_pkg );
2931 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2932 my @display = qw( _items_previous _items_pkg );
2935 foreach my $display ( @display ) {
2936 push @b, $self->$display(@_);
2941 sub _items_previous {
2943 my $cust_main = $self->cust_main;
2944 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2946 foreach ( @pr_cust_bill ) {
2948 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2949 ' ('. time2str('%x',$_->_date). ')',
2950 #'pkgpart' => 'N/A',
2952 'amount' => sprintf("%.2f", $_->owed),
2958 # 'description' => 'Previous Balance',
2959 # #'pkgpart' => 'N/A',
2960 # 'pkgnum' => 'N/A',
2961 # 'amount' => sprintf("%10.2f", $pr_total ),
2962 # 'ext_description' => [ map {
2963 # "Invoice ". $_->invnum.
2964 # " (". time2str("%x",$_->_date). ") ".
2965 # sprintf("%10.2f", $_->owed)
2966 # } @pr_cust_bill ],
2973 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2974 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2978 return 0 unless $a cmp $b;
2979 return -1 if $b eq 'Tax';
2980 return 1 if $a eq 'Tax';
2981 return -1 if $b eq 'Other surcharges';
2982 return 1 if $a eq 'Other surcharges';
2988 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2989 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2992 sub _items_cust_bill_pkg {
2994 my $cust_bill_pkg = shift;
2997 my $format = $opt{format} || '';
2998 my $escape_function = $opt{escape_function} || sub { shift };
2999 my $format_function = $opt{format_function} || '';
3000 my $unsquelched = $opt{unsquelched} || '';
3001 my $section = $opt{section}->{description} if $opt{section};
3004 my ($s, $r, $u) = ( undef, undef, undef );
3005 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3008 foreach ( $s, $r, $u ) {
3009 if ( $_ && !$cust_bill_pkg->hidden ) {
3010 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3011 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3017 foreach my $display ( grep { defined($section)
3018 ? $_->section eq $section
3021 $cust_bill_pkg->cust_bill_pkg_display
3025 my $type = $display->type;
3027 my $desc = $cust_bill_pkg->desc;
3028 $desc = substr($desc, 0, 50). '...'
3029 if $format eq 'latex' && length($desc) > 50;
3031 my %details_opt = ( 'format' => $format,
3032 'escape_function' => $escape_function,
3033 'format_function' => $format_function,
3036 if ( $cust_bill_pkg->pkgnum > 0 ) {
3038 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3040 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3042 my $description = $desc;
3043 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3046 push @d, map &{$escape_function}($_),
3047 $cust_pkg->h_labels_short($self->_date)
3048 unless $cust_pkg->part_pkg->hide_svc_detail
3049 || $cust_bill_pkg->hidden;
3050 push @d, $cust_bill_pkg->details(%details_opt)
3051 if $cust_bill_pkg->recur == 0;
3053 if ( $cust_bill_pkg->hidden ) {
3054 $s->{amount} += $cust_bill_pkg->setup;
3055 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3056 push @{ $s->{ext_description} }, @d;
3059 description => $description,
3060 #pkgpart => $part_pkg->pkgpart,
3061 pkgnum => $cust_bill_pkg->pkgnum,
3062 amount => $cust_bill_pkg->setup,
3063 unit_amount => $cust_bill_pkg->unitsetup,
3064 quantity => $cust_bill_pkg->quantity,
3065 ext_description => \@d,
3071 if ( $cust_bill_pkg->recur != 0 &&
3072 ( !$type || $type eq 'R' || $type eq 'U' )
3076 my $is_summary = $display->summary;
3077 my $description = $is_summary ? "Usage charges" : $desc;
3079 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3080 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3081 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3086 #at least until cust_bill_pkg has "past" ranges in addition to
3087 #the "future" sdate/edate ones... see #3032
3088 my @dates = ( $self->_date );
3089 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3090 push @dates, $prev->sdate if $prev;
3092 push @d, map &{$escape_function}($_),
3093 $cust_pkg->h_labels_short(@dates)
3094 #$cust_bill_pkg->edate,
3095 #$cust_bill_pkg->sdate)
3096 unless $cust_pkg->part_pkg->hide_svc_detail
3097 || $cust_bill_pkg->itemdesc
3098 || $cust_bill_pkg->hidden
3101 push @d, $cust_bill_pkg->details(%details_opt)
3102 unless ($is_summary || $type && $type eq 'R');
3106 $amount = $cust_bill_pkg->recur;
3107 }elsif($type eq 'R') {
3108 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3109 }elsif($type eq 'U') {
3110 $amount = $cust_bill_pkg->usage;
3113 if ( !$type || $type eq 'R' ) {
3115 if ( $cust_bill_pkg->hidden ) {
3116 $r->{amount} += $amount;
3117 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3118 push @{ $r->{ext_description} }, @d;
3121 description => $description,
3122 #pkgpart => $part_pkg->pkgpart,
3123 pkgnum => $cust_bill_pkg->pkgnum,
3125 unit_amount => $cust_bill_pkg->unitrecur,
3126 quantity => $cust_bill_pkg->quantity,
3127 ext_description => \@d,
3131 } elsif ( $amount ) { # && $type eq 'U'
3133 if ( $cust_bill_pkg->hidden ) {
3134 $u->{amount} += $amount;
3135 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3136 push @{ $u->{ext_description} }, @d;
3139 description => $description,
3140 #pkgpart => $part_pkg->pkgpart,
3141 pkgnum => $cust_bill_pkg->pkgnum,
3143 unit_amount => $cust_bill_pkg->unitrecur,
3144 quantity => $cust_bill_pkg->quantity,
3145 ext_description => \@d,
3151 } # recurring or usage with recurring charge
3153 } else { #pkgnum tax or one-shot line item (??)
3155 if ( $cust_bill_pkg->setup != 0 ) {
3157 'description' => $desc,
3158 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3161 if ( $cust_bill_pkg->recur != 0 ) {
3163 'description' => "$desc (".
3164 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3165 time2str("%x", $cust_bill_pkg->edate). ')',
3166 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3176 foreach ( $s, $r, $u ) {
3178 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3179 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3188 sub _items_credits {
3189 my( $self, %opt ) = @_;
3190 my $trim_len = $opt{'trim_len'} || 60;
3194 foreach ( $self->cust_credited ) {
3196 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3198 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3199 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3200 $reason = " ($reason) " if $reason;
3203 #'description' => 'Credit ref\#'. $_->crednum.
3204 # " (". time2str("%x",$_->cust_credit->_date) .")".
3206 'description' => 'Credit applied '.
3207 time2str("%x",$_->cust_credit->_date). $reason,
3208 'amount' => sprintf("%.2f",$_->amount),
3216 sub _items_payments {
3220 #get & print payments
3221 foreach ( $self->cust_bill_pay ) {
3223 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3226 'description' => "Payment received ".
3227 time2str("%x",$_->cust_pay->_date ),
3228 'amount' => sprintf("%.2f", $_->amount )
3236 =item call_details [ OPTION => VALUE ... ]
3238 Returns an array of CSV strings representing the call details for this invoice
3239 The only option available is the boolean prepend_billed_number
3244 my ($self, %opt) = @_;
3246 my $format_function = sub { shift };
3248 if ($opt{prepend_billed_number}) {
3249 $format_function = sub {
3253 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3258 my @details = map { $_->details( 'format_function' => $format_function,
3259 'escape_function' => sub{ return() },
3263 $self->cust_bill_pkg;
3264 my $header = $details[0];
3265 ( $header, grep { $_ ne $header } @details );
3275 =item process_reprint
3279 sub process_reprint {
3280 process_re_X('print', @_);
3283 =item process_reemail
3287 sub process_reemail {
3288 process_re_X('email', @_);
3296 process_re_X('fax', @_);
3304 process_re_X('ftp', @_);
3311 sub process_respool {
3312 process_re_X('spool', @_);
3315 use Storable qw(thaw);
3319 my( $method, $job ) = ( shift, shift );
3320 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3322 my $param = thaw(decode_base64(shift));
3323 warn Dumper($param) if $DEBUG;
3334 my($method, $job, %param ) = @_;
3336 warn "re_X $method for job $job with param:\n".
3337 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3340 #some false laziness w/search/cust_bill.html
3342 my $orderby = 'ORDER BY cust_bill._date';
3344 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3346 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3348 my @cust_bill = qsearch( {
3349 #'select' => "cust_bill.*",
3350 'table' => 'cust_bill',
3351 'addl_from' => $addl_from,
3353 'extra_sql' => $extra_sql,
3354 'order_by' => $orderby,
3358 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3360 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3363 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3364 foreach my $cust_bill ( @cust_bill ) {
3365 $cust_bill->$method();
3367 if ( $job ) { #progressbar foo
3369 if ( time - $min_sec > $last ) {
3370 my $error = $job->update_statustext(
3371 int( 100 * $num / scalar(@cust_bill) )
3373 die $error if $error;
3384 =head1 CLASS METHODS
3390 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3396 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3401 Returns an SQL fragment to retreive the net amount (charged minus credited).
3407 'charged - '. $class->credited_sql;
3412 Returns an SQL fragment to retreive the amount paid against this invoice.
3418 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3419 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3424 Returns an SQL fragment to retreive the amount credited against this invoice.
3430 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3431 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3434 =item search_sql HASHREF
3436 Class method which returns an SQL WHERE fragment to search for parameters
3437 specified in HASHREF. Valid parameters are
3443 Epoch date (UNIX timestamp) setting a lower bound for _date values
3447 Epoch date (UNIX timestamp) setting an upper bound for _date values
3461 =item newest_percust
3465 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3470 my($class, $param) = @_;
3472 warn "$me search_sql called with params: \n".
3473 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3478 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3479 push @search, "cust_bill._date >= $1";
3481 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3482 push @search, "cust_bill._date < $1";
3484 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3485 push @search, "cust_bill.invnum >= $1";
3487 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3488 push @search, "cust_bill.invnum <= $1";
3490 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3491 push @search, "cust_main.agentnum = $1";
3494 push @search, '0 != '. FS::cust_bill->owed_sql
3495 if $param->{'open'};
3497 push @search, '0 != '. FS::cust_bill->net_sql
3500 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3501 if $param->{'days'};
3503 if ( $param->{'newest_percust'} ) {
3505 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3506 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3508 my @newest_where = map { my $x = $_;
3509 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3512 grep ! /^cust_main./, @search;
3513 my $newest_where = scalar(@newest_where)
3514 ? ' AND '. join(' AND ', @newest_where)
3518 push @search, "cust_bill._date = (
3519 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3520 WHERE newest_cust_bill.custnum = cust_bill.custnum
3526 my $curuser = $FS::CurrentUser::CurrentUser;
3527 if ( $curuser->username eq 'fs_queue'
3528 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3530 my $newuser = qsearchs('access_user', {
3531 'username' => $username,
3535 $curuser = $newuser;
3537 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3541 push @search, $curuser->agentnums_sql;
3543 join(' AND ', @search );
3555 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3556 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base