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 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
525 sort { $a->_date <=> $b->_date }
526 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
531 =item cust_credit_bill
533 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
539 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
540 sort { $a->_date <=> $b->_date }
541 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
545 sub cust_credit_bill {
546 shift->cust_credited(@_);
549 =item cust_bill_pay_pkgnum PKGNUM
551 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
552 with matching pkgnum.
556 sub cust_bill_pay_pkgnum {
557 my( $self, $pkgnum ) = @_;
558 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
559 sort { $a->_date <=> $b->_date }
560 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
566 =item cust_credited_pkgnum PKGNUM
568 =item cust_credit_bill_pkgnum PKGNUM
570 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
571 with matching pkgnum.
575 sub cust_credited_pkgnum {
576 my( $self, $pkgnum ) = @_;
577 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
578 sort { $a->_date <=> $b->_date }
579 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
585 sub cust_credit_bill_pkgnum {
586 shift->cust_credited_pkgnum(@_);
591 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
598 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
600 foreach (@taxlines) { $total += $_->setup; }
606 Returns the amount owed (still outstanding) on this invoice, which is charged
607 minus all payment applications (see L<FS::cust_bill_pay>) and credit
608 applications (see L<FS::cust_credit_bill>).
614 my $balance = $self->charged;
615 $balance -= $_->amount foreach ( $self->cust_bill_pay );
616 $balance -= $_->amount foreach ( $self->cust_credited );
617 $balance = sprintf( "%.2f", $balance);
618 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
623 my( $self, $pkgnum ) = @_;
625 #my $balance = $self->charged;
627 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
629 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
630 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
632 $balance = sprintf( "%.2f", $balance);
633 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
637 =item apply_payments_and_credits [ OPTION => VALUE ... ]
639 Applies unapplied payments and credits to this invoice.
641 A hash of optional arguments may be passed. Currently "manual" is supported.
642 If true, a payment receipt is sent instead of a statement when
643 'payment_receipt_email' configuration option is set.
645 If there is an error, returns the error, otherwise returns false.
649 sub apply_payments_and_credits {
650 my( $self, %options ) = @_;
652 local $SIG{HUP} = 'IGNORE';
653 local $SIG{INT} = 'IGNORE';
654 local $SIG{QUIT} = 'IGNORE';
655 local $SIG{TERM} = 'IGNORE';
656 local $SIG{TSTP} = 'IGNORE';
657 local $SIG{PIPE} = 'IGNORE';
659 my $oldAutoCommit = $FS::UID::AutoCommit;
660 local $FS::UID::AutoCommit = 0;
663 $self->select_for_update; #mutex
665 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
666 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
668 if ( $conf->exists('pkg-balances') ) {
669 # limit @payments & @credits to those w/ a pkgnum grepped from $self
670 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
671 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
672 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
675 while ( $self->owed > 0 and ( @payments || @credits ) ) {
678 if ( @payments && @credits ) {
680 #decide which goes first by weight of top (unapplied) line item
682 my @open_lineitems = $self->open_cust_bill_pkg;
685 max( map { $_->part_pkg->pay_weight || 0 }
690 my $max_credit_weight =
691 max( map { $_->part_pkg->credit_weight || 0 }
697 #if both are the same... payments first? it has to be something
698 if ( $max_pay_weight >= $max_credit_weight ) {
704 } elsif ( @payments ) {
706 } elsif ( @credits ) {
709 die "guru meditation #12 and 35";
713 if ( $app eq 'pay' ) {
715 my $payment = shift @payments;
716 $unapp_amount = $payment->unapplied;
717 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
718 $app->pkgnum( $payment->pkgnum )
719 if $conf->exists('pkg-balances') && $payment->pkgnum;
721 } elsif ( $app eq 'credit' ) {
723 my $credit = shift @credits;
724 $unapp_amount = $credit->credited;
725 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
726 $app->pkgnum( $credit->pkgnum )
727 if $conf->exists('pkg-balances') && $credit->pkgnum;
730 die "guru meditation #12 and 35";
734 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
735 warn "owed_pkgnum ". $app->pkgnum;
736 $owed = $self->owed_pkgnum($app->pkgnum);
740 next unless $owed > 0;
742 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
743 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
745 $app->invnum( $self->invnum );
747 my $error = $app->insert(%options);
749 $dbh->rollback if $oldAutoCommit;
750 return "Error inserting ". $app->table. " record: $error";
752 die $error if $error;
756 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
761 =item generate_email OPTION => VALUE ...
769 sender address, required
773 alternate template name, optional
777 text attachment arrayref, optional
781 email subject, optional
785 notice name instead of "Invoice", optional
789 Returns an argument list to be passed to L<FS::Misc::send_email>.
800 my $me = '[FS::cust_bill::generate_email]';
803 'from' => $args{'from'},
804 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
808 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
809 'template' => $args{'template'},
810 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
813 my $cust_main = $self->cust_main;
815 if (ref($args{'to'}) eq 'ARRAY') {
816 $return{'to'} = $args{'to'};
818 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
819 $cust_main->invoicing_list
823 if ( $conf->exists('invoice_html') ) {
825 warn "$me creating HTML/text multipart message"
828 $return{'nobody'} = 1;
830 my $alternative = build MIME::Entity
831 'Type' => 'multipart/alternative',
832 'Encoding' => '7bit',
833 'Disposition' => 'inline'
837 if ( $conf->exists('invoice_email_pdf')
838 and scalar($conf->config('invoice_email_pdf_note')) ) {
840 warn "$me using 'invoice_email_pdf_note' in multipart message"
842 $data = [ map { $_ . "\n" }
843 $conf->config('invoice_email_pdf_note')
848 warn "$me not using 'invoice_email_pdf_note' in multipart message"
850 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
851 $data = $args{'print_text'};
853 $data = [ $self->print_text(\%opt) ];
858 $alternative->attach(
859 'Type' => 'text/plain',
860 #'Encoding' => 'quoted-printable',
861 'Encoding' => '7bit',
863 'Disposition' => 'inline',
866 $args{'from'} =~ /\@([\w\.\-]+)/;
867 my $from = $1 || 'example.com';
868 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
871 my $agentnum = $cust_main->agentnum;
872 if ( defined($args{'template'}) && length($args{'template'})
873 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
876 $logo = 'logo_'. $args{'template'}. '.png';
880 my $image_data = $conf->config_binary( $logo, $agentnum);
882 my $image = build MIME::Entity
883 'Type' => 'image/png',
884 'Encoding' => 'base64',
885 'Data' => $image_data,
886 'Filename' => 'logo.png',
887 'Content-ID' => "<$content_id>",
890 $alternative->attach(
891 'Type' => 'text/html',
892 'Encoding' => 'quoted-printable',
893 'Data' => [ '<html>',
896 ' '. encode_entities($return{'subject'}),
899 ' <body bgcolor="#e8e8e8">',
900 $self->print_html({ 'cid'=>$content_id, %opt }),
904 'Disposition' => 'inline',
905 #'Filename' => 'invoice.pdf',
909 if ( $cust_main->email_csv_cdr ) {
911 push @otherparts, build MIME::Entity
912 'Type' => 'text/csv',
913 'Encoding' => '7bit',
914 'Data' => [ map { "$_\n" }
915 $self->call_details('prepend_billed_number' => 1)
917 'Disposition' => 'attachment',
918 'Filename' => 'usage-'. $self->invnum. '.csv',
923 if ( $conf->exists('invoice_email_pdf') ) {
928 # multipart/alternative
934 my $related = build MIME::Entity 'Type' => 'multipart/related',
935 'Encoding' => '7bit';
937 #false laziness w/Misc::send_email
938 $related->head->replace('Content-type',
940 '; boundary="'. $related->head->multipart_boundary. '"'.
941 '; type=multipart/alternative'
944 $related->add_part($alternative);
946 $related->add_part($image);
948 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
950 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
954 #no other attachment:
956 # multipart/alternative
961 $return{'content-type'} = 'multipart/related';
962 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
963 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
964 #$return{'disposition'} = 'inline';
970 if ( $conf->exists('invoice_email_pdf') ) {
971 warn "$me creating PDF attachment"
974 #mime parts arguments a la MIME::Entity->build().
975 $return{'mimeparts'} = [
976 { $self->mimebuild_pdf(\%opt) }
980 if ( $conf->exists('invoice_email_pdf')
981 and scalar($conf->config('invoice_email_pdf_note')) ) {
983 warn "$me using 'invoice_email_pdf_note'"
985 $return{'body'} = [ map { $_ . "\n" }
986 $conf->config('invoice_email_pdf_note')
991 warn "$me not using 'invoice_email_pdf_note'"
993 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
994 $return{'body'} = $args{'print_text'};
996 $return{'body'} = [ $self->print_text(\%opt) ];
1009 Returns a list suitable for passing to MIME::Entity->build(), representing
1010 this invoice as PDF attachment.
1017 'Type' => 'application/pdf',
1018 'Encoding' => 'base64',
1019 'Data' => [ $self->print_pdf(@_) ],
1020 'Disposition' => 'attachment',
1021 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1025 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1027 Sends this invoice to the destinations configured for this customer: sends
1028 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1030 Options can be passed as a hashref (recommended) or as a list of up to
1031 four values for templatename, agentnum, invoice_from and amount.
1033 I<template>, if specified, is the name of a suffix for alternate invoices.
1035 I<agentnum>, if specified, means that this invoice will only be sent for customers
1036 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1037 single agent) or an arrayref of agentnums.
1039 I<invoice_from>, if specified, overrides the default email invoice From: address.
1041 I<amount>, if specified, only sends the invoice if the total amount owed on this
1042 invoice and all older invoices is greater than the specified amount.
1044 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1048 sub queueable_send {
1051 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1052 or die "invalid invoice number: " . $opt{invnum};
1054 my @args = ( $opt{template}, $opt{agentnum} );
1055 push @args, $opt{invoice_from}
1056 if exists($opt{invoice_from}) && $opt{invoice_from};
1058 my $error = $self->send( @args );
1059 die $error if $error;
1066 my( $template, $invoice_from, $notice_name );
1068 my $balance_over = 0;
1072 $template = $opt->{'template'} || '';
1073 if ( $agentnums = $opt->{'agentnum'} ) {
1074 $agentnums = [ $agentnums ] unless ref($agentnums);
1076 $invoice_from = $opt->{'invoice_from'};
1077 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1078 $notice_name = $opt->{'notice_name'};
1080 $template = scalar(@_) ? shift : '';
1081 if ( scalar(@_) && $_[0] ) {
1082 $agentnums = ref($_[0]) ? shift : [ shift ];
1084 $invoice_from = shift if scalar(@_);
1085 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1088 return 'N/A' unless ! $agentnums
1089 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1092 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1094 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1095 $conf->config('invoice_from', $self->cust_main->agentnum );
1098 'template' => $template,
1099 'invoice_from' => $invoice_from,
1100 'notice_name' => ( $notice_name || 'Invoice' ),
1103 my @invoicing_list = $self->cust_main->invoicing_list;
1105 #$self->email_invoice(\%opt)
1107 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1109 #$self->print_invoice(\%opt)
1111 if grep { $_ eq 'POST' } @invoicing_list; #postal
1113 $self->fax_invoice(\%opt)
1114 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1120 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1122 Emails this invoice.
1124 Options can be passed as a hashref (recommended) or as a list of up to
1125 two values for templatename and invoice_from.
1127 I<template>, if specified, is the name of a suffix for alternate invoices.
1129 I<invoice_from>, if specified, overrides the default email invoice From: address.
1131 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1135 sub queueable_email {
1138 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1139 or die "invalid invoice number: " . $opt{invnum};
1141 my @args = ( $opt{template} );
1142 push @args, $opt{invoice_from}
1143 if exists($opt{invoice_from}) && $opt{invoice_from};
1145 my $error = $self->email( @args );
1146 die $error if $error;
1150 #sub email_invoice {
1154 my( $template, $invoice_from, $notice_name );
1157 $template = $opt->{'template'} || '';
1158 $invoice_from = $opt->{'invoice_from'};
1159 $notice_name = $opt->{'notice_name'} || 'Invoice';
1161 $template = scalar(@_) ? shift : '';
1162 $invoice_from = shift if scalar(@_);
1163 $notice_name = 'Invoice';
1166 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1167 $conf->config('invoice_from', $self->cust_main->agentnum );
1169 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1170 $self->cust_main->invoicing_list;
1172 #better to notify this person than silence
1173 @invoicing_list = ($invoice_from) unless @invoicing_list;
1175 my $subject = $self->email_subject($template);
1177 my $error = send_email(
1178 $self->generate_email(
1179 'from' => $invoice_from,
1180 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1181 'subject' => $subject,
1182 'template' => $template,
1183 'notice_name' => $notice_name,
1186 die "can't email invoice: $error\n" if $error;
1187 #die "$error\n" if $error;
1194 #my $template = scalar(@_) ? shift : '';
1197 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1200 my $cust_main = $self->cust_main;
1201 my $name = $cust_main->name;
1202 my $name_short = $cust_main->name_short;
1203 my $invoice_number = $self->invnum;
1204 my $invoice_date = $self->_date_pretty;
1206 eval qq("$subject");
1209 =item lpr_data HASHREF | [ TEMPLATE ]
1211 Returns the postscript or plaintext for this invoice as an arrayref.
1213 Options can be passed as a hashref (recommended) or as a single optional value
1216 I<template>, if specified, is the name of a suffix for alternate invoices.
1218 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1224 my( $template, $notice_name );
1227 $template = $opt->{'template'} || '';
1228 $notice_name = $opt->{'notice_name'} || 'Invoice';
1230 $template = scalar(@_) ? shift : '';
1231 $notice_name = 'Invoice';
1235 'template' => $template,
1236 'notice_name' => $notice_name,
1239 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1240 [ $self->$method( \%opt ) ];
1243 =item print HASHREF | [ TEMPLATE ]
1245 Prints this invoice.
1247 Options can be passed as a hashref (recommended) or as a single optional
1250 I<template>, if specified, is the name of a suffix for alternate invoices.
1252 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1256 #sub print_invoice {
1259 my( $template, $notice_name );
1262 $template = $opt->{'template'} || '';
1263 $notice_name = $opt->{'notice_name'} || 'Invoice';
1265 $template = scalar(@_) ? shift : '';
1266 $notice_name = 'Invoice';
1270 'template' => $template,
1271 'notice_name' => $notice_name,
1274 do_print $self->lpr_data(\%opt);
1277 =item fax_invoice HASHREF | [ TEMPLATE ]
1281 Options can be passed as a hashref (recommended) or as a single optional
1284 I<template>, if specified, is the name of a suffix for alternate invoices.
1286 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1292 my( $template, $notice_name );
1295 $template = $opt->{'template'} || '';
1296 $notice_name = $opt->{'notice_name'} || 'Invoice';
1298 $template = scalar(@_) ? shift : '';
1299 $notice_name = 'Invoice';
1302 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1303 unless $conf->exists('invoice_latex');
1305 my $dialstring = $self->cust_main->getfield('fax');
1309 'template' => $template,
1310 'notice_name' => $notice_name,
1313 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1314 'dialstring' => $dialstring,
1316 die $error if $error;
1320 =item ftp_invoice [ TEMPLATENAME ]
1322 Sends this invoice data via FTP.
1324 TEMPLATENAME is unused?
1330 my $template = scalar(@_) ? shift : '';
1333 'protocol' => 'ftp',
1334 'server' => $conf->config('cust_bill-ftpserver'),
1335 'username' => $conf->config('cust_bill-ftpusername'),
1336 'password' => $conf->config('cust_bill-ftppassword'),
1337 'dir' => $conf->config('cust_bill-ftpdir'),
1338 'format' => $conf->config('cust_bill-ftpformat'),
1342 =item spool_invoice [ TEMPLATENAME ]
1344 Spools this invoice data (see L<FS::spool_csv>)
1346 TEMPLATENAME is unused?
1352 my $template = scalar(@_) ? shift : '';
1355 'format' => $conf->config('cust_bill-spoolformat'),
1356 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1360 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1362 Like B<send>, but only sends the invoice if it is the newest open invoice for
1367 sub send_if_newest {
1372 grep { $_->owed > 0 }
1373 qsearch('cust_bill', {
1374 'custnum' => $self->custnum,
1375 #'_date' => { op=>'>', value=>$self->_date },
1376 'invnum' => { op=>'>', value=>$self->invnum },
1383 =item send_csv OPTION => VALUE, ...
1385 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1389 protocol - currently only "ftp"
1395 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1396 and YYMMDDHHMMSS is a timestamp.
1398 See L</print_csv> for a description of the output format.
1403 my($self, %opt) = @_;
1407 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1408 mkdir $spooldir, 0700 unless -d $spooldir;
1410 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1411 my $file = "$spooldir/$tracctnum.csv";
1413 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1415 open(CSV, ">$file") or die "can't open $file: $!";
1423 if ( $opt{protocol} eq 'ftp' ) {
1424 eval "use Net::FTP;";
1426 $net = Net::FTP->new($opt{server}) or die @$;
1428 die "unknown protocol: $opt{protocol}";
1431 $net->login( $opt{username}, $opt{password} )
1432 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1434 $net->binary or die "can't set binary mode";
1436 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1438 $net->put($file) or die "can't put $file: $!";
1448 Spools CSV invoice data.
1454 =item format - 'default' or 'billco'
1456 =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>).
1458 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1460 =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.
1467 my($self, %opt) = @_;
1469 my $cust_main = $self->cust_main;
1471 if ( $opt{'dest'} ) {
1472 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1473 $cust_main->invoicing_list;
1474 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1475 || ! keys %invoicing_list;
1478 if ( $opt{'balanceover'} ) {
1480 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1483 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1484 mkdir $spooldir, 0700 unless -d $spooldir;
1486 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1490 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1491 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1494 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1496 open(CSV, ">>$file") or die "can't open $file: $!";
1497 flock(CSV, LOCK_EX);
1502 if ( lc($opt{'format'}) eq 'billco' ) {
1504 flock(CSV, LOCK_UN);
1509 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1512 open(CSV,">>$file") or die "can't open $file: $!";
1513 flock(CSV, LOCK_EX);
1519 flock(CSV, LOCK_UN);
1526 =item print_csv OPTION => VALUE, ...
1528 Returns CSV data for this invoice.
1532 format - 'default' or 'billco'
1534 Returns a list consisting of two scalars. The first is a single line of CSV
1535 header information for this invoice. The second is one or more lines of CSV
1536 detail information for this invoice.
1538 If I<format> is not specified or "default", the fields of the CSV file are as
1541 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1545 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1547 B<record_type> is C<cust_bill> for the initial header line only. The
1548 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1549 fields are filled in.
1551 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1552 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1555 =item invnum - invoice number
1557 =item custnum - customer number
1559 =item _date - invoice date
1561 =item charged - total invoice amount
1563 =item first - customer first name
1565 =item last - customer first name
1567 =item company - company name
1569 =item address1 - address line 1
1571 =item address2 - address line 1
1581 =item pkg - line item description
1583 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1585 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1587 =item sdate - start date for recurring fee
1589 =item edate - end date for recurring fee
1593 If I<format> is "billco", the fields of the header CSV file are as follows:
1595 +-------------------------------------------------------------------+
1596 | FORMAT HEADER FILE |
1597 |-------------------------------------------------------------------|
1598 | Field | Description | Name | Type | Width |
1599 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1600 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1601 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1602 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1603 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1604 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1605 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1606 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1607 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1608 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1609 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1610 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1611 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1612 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1613 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1614 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1615 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1616 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1617 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1618 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1619 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1620 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1621 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1622 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1623 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1624 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1625 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1626 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1627 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1628 +-------+-------------------------------+------------+------+-------+
1630 If I<format> is "billco", the fields of the detail CSV file are as follows:
1632 FORMAT FOR DETAIL FILE
1634 Field | Description | Name | Type | Width
1635 1 | N/A-Leave Empty | RC | CHAR | 2
1636 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1637 3 | Account Number | TRACCTNUM | CHAR | 15
1638 4 | Invoice Number | TRINVOICE | CHAR | 15
1639 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1640 6 | Transaction Detail | DETAILS | CHAR | 100
1641 7 | Amount | AMT | NUM* | 9
1642 8 | Line Format Control** | LNCTRL | CHAR | 2
1643 9 | Grouping Code | GROUP | CHAR | 2
1644 10 | User Defined | ACCT CODE | CHAR | 15
1649 my($self, %opt) = @_;
1651 eval "use Text::CSV_XS";
1654 my $cust_main = $self->cust_main;
1656 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1658 if ( lc($opt{'format'}) eq 'billco' ) {
1661 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1663 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1665 my( $previous_balance, @unused ) = $self->previous; #previous balance
1667 my $pmt_cr_applied = 0;
1668 $pmt_cr_applied += $_->{'amount'}
1669 foreach ( $self->_items_payments, $self->_items_credits ) ;
1671 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1674 '', # 1 | N/A-Leave Empty CHAR 2
1675 '', # 2 | N/A-Leave Empty CHAR 15
1676 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1677 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1678 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1679 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1680 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1681 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1682 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1683 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1684 '', # 10 | Ancillary Billing Information CHAR 30
1685 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1686 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1689 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1692 $duedate, # 14 | Bill Due Date CHAR 10
1694 $previous_balance, # 15 | Previous Balance NUM* 9
1695 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1696 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1697 $totaldue, # 18 | Total Amt Due NUM* 9
1698 $totaldue, # 19 | Total Amt Due NUM* 9
1699 '', # 20 | 30 Day Aging NUM* 9
1700 '', # 21 | 60 Day Aging NUM* 9
1701 '', # 22 | 90 Day Aging NUM* 9
1702 'N', # 23 | Y/N CHAR 1
1703 '', # 24 | Remittance automation CHAR 100
1704 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1705 $self->custnum, # 26 | Customer Reference Number CHAR 15
1706 '0', # 27 | Federal Tax*** NUM* 9
1707 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1708 '0', # 29 | Other Taxes & Fees*** NUM* 9
1717 time2str("%x", $self->_date),
1718 sprintf("%.2f", $self->charged),
1719 ( map { $cust_main->getfield($_) }
1720 qw( first last company address1 address2 city state zip country ) ),
1722 ) or die "can't create csv";
1725 my $header = $csv->string. "\n";
1728 if ( lc($opt{'format'}) eq 'billco' ) {
1731 foreach my $item ( $self->_items_pkg ) {
1734 '', # 1 | N/A-Leave Empty CHAR 2
1735 '', # 2 | N/A-Leave Empty CHAR 15
1736 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1737 $self->invnum, # 4 | Invoice Number CHAR 15
1738 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1739 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1740 $item->{'amount'}, # 7 | Amount NUM* 9
1741 '', # 8 | Line Format Control** CHAR 2
1742 '', # 9 | Grouping Code CHAR 2
1743 '', # 10 | User Defined CHAR 15
1746 $detail .= $csv->string. "\n";
1752 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1754 my($pkg, $setup, $recur, $sdate, $edate);
1755 if ( $cust_bill_pkg->pkgnum ) {
1757 ($pkg, $setup, $recur, $sdate, $edate) = (
1758 $cust_bill_pkg->part_pkg->pkg,
1759 ( $cust_bill_pkg->setup != 0
1760 ? sprintf("%.2f", $cust_bill_pkg->setup )
1762 ( $cust_bill_pkg->recur != 0
1763 ? sprintf("%.2f", $cust_bill_pkg->recur )
1765 ( $cust_bill_pkg->sdate
1766 ? time2str("%x", $cust_bill_pkg->sdate)
1768 ($cust_bill_pkg->edate
1769 ?time2str("%x", $cust_bill_pkg->edate)
1773 } else { #pkgnum tax
1774 next unless $cust_bill_pkg->setup != 0;
1775 $pkg = $cust_bill_pkg->desc;
1776 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1777 ( $sdate, $edate ) = ( '', '' );
1783 ( map { '' } (1..11) ),
1784 ($pkg, $setup, $recur, $sdate, $edate)
1785 ) or die "can't create csv";
1787 $detail .= $csv->string. "\n";
1793 ( $header, $detail );
1799 Pays this invoice with a compliemntary payment. If there is an error,
1800 returns the error, otherwise returns false.
1806 my $cust_pay = new FS::cust_pay ( {
1807 'invnum' => $self->invnum,
1808 'paid' => $self->owed,
1811 'payinfo' => $self->cust_main->payinfo,
1819 Attempts to pay this invoice with a credit card payment via a
1820 Business::OnlinePayment realtime gateway. See
1821 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1822 for supported processors.
1828 $self->realtime_bop( 'CC', @_ );
1833 Attempts to pay this invoice with an electronic check (ACH) payment via a
1834 Business::OnlinePayment realtime gateway. See
1835 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1836 for supported processors.
1842 $self->realtime_bop( 'ECHECK', @_ );
1847 Attempts to pay this invoice with phone bill (LEC) payment via a
1848 Business::OnlinePayment realtime gateway. See
1849 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1850 for supported processors.
1856 $self->realtime_bop( 'LEC', @_ );
1860 my( $self, $method ) = @_;
1862 my $cust_main = $self->cust_main;
1863 my $balance = $cust_main->balance;
1864 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1865 $amount = sprintf("%.2f", $amount);
1866 return "not run (balance $balance)" unless $amount > 0;
1868 my $description = 'Internet Services';
1869 if ( $conf->exists('business-onlinepayment-description') ) {
1870 my $dtempl = $conf->config('business-onlinepayment-description');
1872 my $agent_obj = $cust_main->agent
1873 or die "can't retreive agent for $cust_main (agentnum ".
1874 $cust_main->agentnum. ")";
1875 my $agent = $agent_obj->agent;
1876 my $pkgs = join(', ',
1877 map { $_->part_pkg->pkg }
1878 grep { $_->pkgnum } $self->cust_bill_pkg
1880 $description = eval qq("$dtempl");
1883 $cust_main->realtime_bop($method, $amount,
1884 'description' => $description,
1885 'invnum' => $self->invnum,
1890 =item batch_card OPTION => VALUE...
1892 Adds a payment for this invoice to the pending credit card batch (see
1893 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1894 runs the payment using a realtime gateway.
1899 my ($self, %options) = @_;
1900 my $cust_main = $self->cust_main;
1902 $options{invnum} = $self->invnum;
1904 $cust_main->batch_card(%options);
1907 sub _agent_template {
1909 $self->cust_main->agent_template;
1912 sub _agent_invoice_from {
1914 $self->cust_main->agent_invoice_from;
1917 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1919 Returns an text invoice, as a list of lines.
1921 Options can be passed as a hashref (recommended) or as a list of time, template
1922 and then any key/value pairs for any other options.
1924 I<time>, if specified, is used to control the printing of overdue messages. The
1925 default is now. It isn't the date of the invoice; that's the `_date' field.
1926 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1927 L<Time::Local> and L<Date::Parse> for conversion functions.
1929 I<template>, if specified, is the name of a suffix for alternate invoices.
1931 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1937 my( $today, $template, %opt );
1939 %opt = %{ shift() };
1940 $today = delete($opt{'time'}) || '';
1941 $template = delete($opt{template}) || '';
1943 ( $today, $template, %opt ) = @_;
1946 my %params = ( 'format' => 'template' );
1947 $params{'time'} = $today if $today;
1948 $params{'template'} = $template if $template;
1949 $params{$_} = $opt{$_}
1950 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1952 $self->print_generic( %params );
1955 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1957 Internal method - returns a filename of a filled-in LaTeX template for this
1958 invoice (Note: add ".tex" to get the actual filename), and a filename of
1959 an associated logo (with the .eps extension included).
1961 See print_ps and print_pdf for methods that return PostScript and PDF output.
1963 Options can be passed as a hashref (recommended) or as a list of time, template
1964 and then any key/value pairs for any other options.
1966 I<time>, if specified, is used to control the printing of overdue messages. The
1967 default is now. It isn't the date of the invoice; that's the `_date' field.
1968 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1969 L<Time::Local> and L<Date::Parse> for conversion functions.
1971 I<template>, if specified, is the name of a suffix for alternate invoices.
1973 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1979 my( $today, $template, %opt );
1981 %opt = %{ shift() };
1982 $today = delete($opt{'time'}) || '';
1983 $template = delete($opt{template}) || '';
1985 ( $today, $template, %opt ) = @_;
1988 my %params = ( 'format' => 'latex' );
1989 $params{'time'} = $today if $today;
1990 $params{'template'} = $template if $template;
1991 $params{$_} = $opt{$_}
1992 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1994 $template ||= $self->_agent_template;
1996 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1997 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2001 ) or die "can't open temp file: $!\n";
2003 my $agentnum = $self->cust_main->agentnum;
2005 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2006 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2007 or die "can't write temp file: $!\n";
2009 print $lh $conf->config_binary('logo.eps', $agentnum)
2010 or die "can't write temp file: $!\n";
2013 $params{'logo_file'} = $lh->filename;
2015 my @filled_in = $self->print_generic( %params );
2017 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2021 ) or die "can't open temp file: $!\n";
2022 print $fh join('', @filled_in );
2025 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2026 return ($1, $params{'logo_file'});
2030 =item print_generic OPTION => VALUE ...
2032 Internal method - returns a filled-in template for this invoice as a scalar.
2034 See print_ps and print_pdf for methods that return PostScript and PDF output.
2036 Non optional options include
2037 format - latex, html, template
2039 Optional options include
2041 template - a value used as a suffix for a configuration template
2043 time - a value used to control the printing of overdue messages. The
2044 default is now. It isn't the date of the invoice; that's the `_date' field.
2045 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2046 L<Time::Local> and L<Date::Parse> for conversion functions.
2050 unsquelch_cdr - overrides any per customer cdr squelching when true
2052 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2056 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2057 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2060 my( $self, %params ) = @_;
2061 my $today = $params{today} ? $params{today} : time;
2062 warn "$me print_generic called on $self with suffix $params{template}\n"
2065 my $format = $params{format};
2066 die "Unknown format: $format"
2067 unless $format =~ /^(latex|html|template)$/;
2069 my $cust_main = $self->cust_main;
2070 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2071 unless $cust_main->payname
2072 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2074 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2075 'html' => [ '<%=', '%>' ],
2076 'template' => [ '{', '}' ],
2079 #create the template
2080 my $template = $params{template} ? $params{template} : $self->_agent_template;
2081 my $templatefile = "invoice_$format";
2082 $templatefile .= "_$template"
2083 if length($template);
2084 my @invoice_template = map "$_\n", $conf->config($templatefile)
2085 or die "cannot load config data $templatefile";
2088 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2089 #change this to a die when the old code is removed
2090 warn "old-style invoice template $templatefile; ".
2091 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2092 $old_latex = 'true';
2093 @invoice_template = _translate_old_latex_format(@invoice_template);
2096 my $text_template = new Text::Template(
2098 SOURCE => \@invoice_template,
2099 DELIMITERS => $delimiters{$format},
2102 $text_template->compile()
2103 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2106 # additional substitution could possibly cause breakage in existing templates
2107 my %convert_maps = (
2109 'notes' => sub { map "$_", @_ },
2110 'footer' => sub { map "$_", @_ },
2111 'smallfooter' => sub { map "$_", @_ },
2112 'returnaddress' => sub { map "$_", @_ },
2113 'coupon' => sub { map "$_", @_ },
2114 'summary' => sub { map "$_", @_ },
2120 s/%%(.*)$/<!-- $1 -->/g;
2121 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2122 s/\\begin\{enumerate\}/<ol>/g;
2124 s/\\end\{enumerate\}/<\/ol>/g;
2125 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2134 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2136 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2141 s/\\\\\*?\s*$/<BR>/;
2142 s/\\hyphenation\{[\w\s\-]+}//;
2147 'coupon' => sub { "" },
2148 'summary' => sub { "" },
2155 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2156 s/\\begin\{enumerate\}//g;
2158 s/\\end\{enumerate\}//g;
2159 s/\\textbf\{(.*)\}/$1/g;
2166 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2168 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2173 s/\\\\\*?\s*$/\n/; # dubious
2174 s/\\hyphenation\{[\w\s\-]+}//;
2178 'coupon' => sub { "" },
2179 'summary' => sub { "" },
2184 # hashes for differing output formats
2185 my %nbsps = ( 'latex' => '~',
2186 'html' => '', # '&nbps;' would be nice
2187 'template' => '', # not used
2189 my $nbsp = $nbsps{$format};
2191 my %escape_functions = ( 'latex' => \&_latex_escape,
2192 'html' => \&encode_entities,
2193 'template' => sub { shift },
2195 my $escape_function = $escape_functions{$format};
2197 my %date_formats = ( 'latex' => '%b %o, %Y',
2198 'html' => '%b %o, %Y',
2201 my $date_format = $date_formats{$format};
2203 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2205 'html' => sub { return '<b>'. shift(). '</b>'
2207 'template' => sub { shift },
2209 my $embolden_function = $embolden_functions{$format};
2212 # generate template variables
2215 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2219 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2225 $returnaddress = join("\n",
2226 $conf->config_orbase("invoice_${format}returnaddress", $template)
2229 } elsif ( grep /\S/,
2230 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2232 my $convert_map = $convert_maps{$format}{'returnaddress'};
2235 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2240 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2242 my $convert_map = $convert_maps{$format}{'returnaddress'};
2243 $returnaddress = join( "\n", &$convert_map(
2244 map { s/( {2,})/'~' x length($1)/eg;
2248 ( $conf->config('company_name', $self->cust_main->agentnum),
2249 $conf->config('company_address', $self->cust_main->agentnum),
2256 my $warning = "Couldn't find a return address; ".
2257 "do you need to set the company_address configuration value?";
2259 $returnaddress = $nbsp;
2260 #$returnaddress = $warning;
2264 my %invoice_data = (
2267 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2268 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2269 'returnaddress' => $returnaddress,
2270 'agent' => &$escape_function($cust_main->agent->agent),
2273 'invnum' => $self->invnum,
2274 'date' => time2str($date_format, $self->_date),
2275 'today' => time2str('%b %o, %Y', $today),
2276 'terms' => $self->terms,
2277 'template' => $template, #params{'template'},
2278 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2279 'current_charges' => sprintf("%.2f", $self->charged),
2280 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2283 'custnum' => $cust_main->display_custnum,
2284 'agent_custid' => &$escape_function($cust_main->agent_custid),
2285 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2286 payname company address1 address2 city state zip fax
2290 'ship_enable' => $conf->exists('invoice-ship_address'),
2291 'unitprices' => $conf->exists('invoice-unitprice'),
2292 'smallernotes' => $conf->exists('invoice-smallernotes'),
2293 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2295 # better hang on to conf_dir for a while (for old templates)
2296 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2298 #these are only used when doing paged plaintext
2304 $invoice_data{finance_section} = '';
2305 if ( $conf->config('finance_pkgclass') ) {
2307 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2308 $invoice_data{finance_section} = $pkg_class->categoryname;
2310 $invoice_data{finance_amount} = '0.00';
2312 my $countrydefault = $conf->config('countrydefault') || 'US';
2313 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2314 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2315 my $method = $prefix.$_;
2316 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2318 $invoice_data{'ship_country'} = ''
2319 if ( $invoice_data{'ship_country'} eq $countrydefault );
2321 $invoice_data{'cid'} = $params{'cid'}
2324 if ( $cust_main->country eq $countrydefault ) {
2325 $invoice_data{'country'} = '';
2327 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2331 $invoice_data{'address'} = \@address;
2333 $cust_main->payname.
2334 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2335 ? " (P.O. #". $cust_main->payinfo. ")"
2339 push @address, $cust_main->company
2340 if $cust_main->company;
2341 push @address, $cust_main->address1;
2342 push @address, $cust_main->address2
2343 if $cust_main->address2;
2345 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2346 push @address, $invoice_data{'country'}
2347 if $invoice_data{'country'};
2349 while (scalar(@address) < 5);
2351 $invoice_data{'logo_file'} = $params{'logo_file'}
2352 if $params{'logo_file'};
2354 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2355 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2356 #my $balance_due = $self->owed + $pr_total - $cr_total;
2357 my $balance_due = $self->owed + $pr_total;
2358 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2359 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2360 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2361 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2363 my $agentnum = $self->cust_main->agentnum;
2365 my $summarypage = '';
2366 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2369 $invoice_data{'summarypage'} = $summarypage;
2371 #do variable substitution in notes, footer, smallfooter
2372 foreach my $include (qw( notes footer smallfooter coupon )) {
2374 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2377 if ( $conf->exists($inc_file, $agentnum)
2378 && length( $conf->config($inc_file, $agentnum) ) ) {
2380 @inc_src = $conf->config($inc_file, $agentnum);
2384 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2386 my $convert_map = $convert_maps{$format}{$include};
2388 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2389 s/--\@\]/$delimiters{$format}[1]/g;
2392 &$convert_map( $conf->config($inc_file, $agentnum) );
2396 my $inc_tt = new Text::Template (
2398 SOURCE => [ map "$_\n", @inc_src ],
2399 DELIMITERS => $delimiters{$format},
2400 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2402 unless ( $inc_tt->compile() ) {
2403 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2404 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2408 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2410 $invoice_data{$include} =~ s/\n+$//
2411 if ($format eq 'latex');
2414 $invoice_data{'po_line'} =
2415 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2416 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2419 my %money_chars = ( 'latex' => '',
2420 'html' => $conf->config('money_char') || '$',
2423 my $money_char = $money_chars{$format};
2425 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2426 'html' => $conf->config('money_char') || '$',
2429 my $other_money_char = $other_money_chars{$format};
2430 $invoice_data{'dollar'} = $other_money_char;
2432 my @detail_items = ();
2433 my @total_items = ();
2437 $invoice_data{'detail_items'} = \@detail_items;
2438 $invoice_data{'total_items'} = \@total_items;
2439 $invoice_data{'buf'} = \@buf;
2440 $invoice_data{'sections'} = \@sections;
2442 my $previous_section = { 'description' => 'Previous Charges',
2443 'subtotal' => $other_money_char.
2444 sprintf('%.2f', $pr_total),
2445 'summarized' => $summarypage ? 'Y' : '',
2449 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2450 'subtotal' => $taxtotal, # adjusted below
2451 'summarized' => $summarypage ? 'Y' : '',
2454 my $adjusttotal = 0;
2455 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2456 'subtotal' => 0, # adjusted below
2457 'summarized' => $summarypage ? 'Y' : '',
2460 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2461 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2462 my $late_sections = [];
2463 if ( $multisection ) {
2465 $self->_items_sections( $late_sections, $summarypage, $escape_function );
2467 push @sections, { 'description' => '', 'subtotal' => '' };
2470 unless ( $conf->exists('disable_previous_balance')
2471 || $conf->exists('previous_balance-summary_only')
2475 foreach my $line_item ( $self->_items_previous ) {
2478 ext_description => [],
2480 $detail->{'ref'} = $line_item->{'pkgnum'};
2481 $detail->{'quantity'} = 1;
2482 $detail->{'section'} = $previous_section;
2483 $detail->{'description'} = &$escape_function($line_item->{'description'});
2484 if ( exists $line_item->{'ext_description'} ) {
2485 @{$detail->{'ext_description'}} = map {
2486 &$escape_function($_);
2487 } @{$line_item->{'ext_description'}};
2489 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2490 $line_item->{'amount'};
2491 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2493 push @detail_items, $detail;
2494 push @buf, [ $detail->{'description'},
2495 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2501 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2502 push @buf, ['','-----------'];
2503 push @buf, [ 'Total Previous Balance',
2504 $money_char. sprintf("%10.2f", $pr_total) ];
2508 foreach my $section (@sections, @$late_sections) {
2510 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2511 if ( $invoice_data{finance_section} &&
2512 $section->{'description'} eq $invoice_data{finance_section} );
2514 $section->{'subtotal'} = $other_money_char.
2515 sprintf('%.2f', $section->{'subtotal'})
2518 if ( $section->{'description'} ) {
2519 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2525 $options{'section'} = $section if $multisection;
2526 $options{'format'} = $format;
2527 $options{'escape_function'} = $escape_function;
2528 $options{'format_function'} = sub { () } unless $unsquelched;
2529 $options{'unsquelched'} = $unsquelched;
2530 $options{'summary_page'} = $summarypage;
2532 foreach my $line_item ( $self->_items_pkg(%options) ) {
2534 ext_description => [],
2536 $detail->{'ref'} = $line_item->{'pkgnum'};
2537 $detail->{'quantity'} = $line_item->{'quantity'};
2538 $detail->{'section'} = $section;
2539 $detail->{'description'} = &$escape_function($line_item->{'description'});
2540 if ( exists $line_item->{'ext_description'} ) {
2541 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2543 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2544 $line_item->{'amount'};
2545 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2546 $line_item->{'unit_amount'};
2547 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2549 push @detail_items, $detail;
2550 push @buf, ( [ $detail->{'description'},
2551 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2553 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2557 if ( $section->{'description'} ) {
2558 push @buf, ( ['','-----------'],
2559 [ $section->{'description'}. ' sub-total',
2560 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2569 $invoice_data{current_less_finance} =
2570 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2572 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2573 unshift @sections, $previous_section if $pr_total;
2576 foreach my $tax ( $self->_items_tax ) {
2578 $taxtotal += $tax->{'amount'};
2580 my $description = &$escape_function( $tax->{'description'} );
2581 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2583 if ( $multisection ) {
2585 my $money = $old_latex ? '' : $money_char;
2586 push @detail_items, {
2587 ext_description => [],
2590 description => $description,
2591 amount => $money. $amount,
2593 section => $tax_section,
2598 push @total_items, {
2599 'total_item' => $description,
2600 'total_amount' => $other_money_char. $amount,
2605 push @buf,[ $description,
2606 $money_char. $amount,
2613 $total->{'total_item'} = 'Sub-total';
2614 $total->{'total_amount'} =
2615 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2617 if ( $multisection ) {
2618 $tax_section->{'subtotal'} = $other_money_char.
2619 sprintf('%.2f', $taxtotal);
2620 $tax_section->{'pretotal'} = 'New charges sub-total '.
2621 $total->{'total_amount'};
2622 push @sections, $tax_section if $taxtotal;
2624 unshift @total_items, $total;
2627 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2629 push @buf,['','-----------'];
2630 push @buf,[( $conf->exists('disable_previous_balance')
2632 : 'Total New Charges'
2634 $money_char. sprintf("%10.2f",$self->charged) ];
2639 $total->{'total_item'} = &$embolden_function('Total');
2640 $total->{'total_amount'} =
2641 &$embolden_function(
2644 $self->charged + ( $conf->exists('disable_previous_balance')
2650 if ( $multisection ) {
2651 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2652 sprintf('%.2f', $self->charged );
2654 push @total_items, $total;
2656 push @buf,['','-----------'];
2657 push @buf,['Total Charges',
2659 sprintf( '%10.2f', $self->charged +
2660 ( $conf->exists('disable_previous_balance')
2669 unless ( $conf->exists('disable_previous_balance') ) {
2670 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2673 my $credittotal = 0;
2674 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2677 $total->{'total_item'} = &$escape_function($credit->{'description'});
2678 $credittotal += $credit->{'amount'};
2679 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2680 $adjusttotal += $credit->{'amount'};
2681 if ( $multisection ) {
2682 my $money = $old_latex ? '' : $money_char;
2683 push @detail_items, {
2684 ext_description => [],
2687 description => &$escape_function($credit->{'description'}),
2688 amount => $money. $credit->{'amount'},
2690 section => $adjust_section,
2693 push @total_items, $total;
2697 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2700 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2701 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2705 my $paymenttotal = 0;
2706 foreach my $payment ( $self->_items_payments ) {
2708 $total->{'total_item'} = &$escape_function($payment->{'description'});
2709 $paymenttotal += $payment->{'amount'};
2710 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2711 $adjusttotal += $payment->{'amount'};
2712 if ( $multisection ) {
2713 my $money = $old_latex ? '' : $money_char;
2714 push @detail_items, {
2715 ext_description => [],
2718 description => &$escape_function($payment->{'description'}),
2719 amount => $money. $payment->{'amount'},
2721 section => $adjust_section,
2724 push @total_items, $total;
2726 push @buf, [ $payment->{'description'},
2727 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2730 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2732 if ( $multisection ) {
2733 $adjust_section->{'subtotal'} = $other_money_char.
2734 sprintf('%.2f', $adjusttotal);
2735 push @sections, $adjust_section;
2740 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2741 $total->{'total_amount'} =
2742 &$embolden_function(
2743 $other_money_char. sprintf('%.2f', $summarypage
2745 $self->billing_balance
2746 : $self->owed + $pr_total
2749 if ( $multisection ) {
2750 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2751 $total->{'total_amount'};
2753 push @total_items, $total;
2755 push @buf,['','-----------'];
2756 push @buf,[$self->balance_due_msg, $money_char.
2757 sprintf("%10.2f", $balance_due ) ];
2761 if ( $multisection ) {
2762 push @sections, @$late_sections
2766 my @includelist = ();
2767 push @includelist, 'summary' if $summarypage;
2768 foreach my $include ( @includelist ) {
2770 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2773 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2775 @inc_src = $conf->config($inc_file, $agentnum);
2779 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2781 my $convert_map = $convert_maps{$format}{$include};
2783 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2784 s/--\@\]/$delimiters{$format}[1]/g;
2787 &$convert_map( $conf->config($inc_file, $agentnum) );
2791 my $inc_tt = new Text::Template (
2793 SOURCE => [ map "$_\n", @inc_src ],
2794 DELIMITERS => $delimiters{$format},
2795 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2797 unless ( $inc_tt->compile() ) {
2798 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2799 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2803 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2805 $invoice_data{$include} =~ s/\n+$//
2806 if ($format eq 'latex');
2811 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2812 /invoice_lines\((\d*)\)/;
2813 $invoice_lines += $1 || scalar(@buf);
2816 die "no invoice_lines() functions in template?"
2817 if ( $format eq 'template' && !$wasfunc );
2819 if ($format eq 'template') {
2821 if ( $invoice_lines ) {
2822 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2823 $invoice_data{'total_pages'}++
2824 if scalar(@buf) % $invoice_lines;
2827 #setup subroutine for the template
2828 sub FS::cust_bill::_template::invoice_lines {
2829 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2831 scalar(@FS::cust_bill::_template::buf)
2832 ? shift @FS::cust_bill::_template::buf
2841 push @collect, split("\n",
2842 $text_template->fill_in( HASH => \%invoice_data,
2843 PACKAGE => 'FS::cust_bill::_template'
2846 $FS::cust_bill::_template::page++;
2848 map "$_\n", @collect;
2850 warn "filling in template for invoice ". $self->invnum. "\n"
2852 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2855 $text_template->fill_in(HASH => \%invoice_data);
2859 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2861 Returns an postscript invoice, as a scalar.
2863 Options can be passed as a hashref (recommended) or as a list of time, template
2864 and then any key/value pairs for any other options.
2866 I<time> an optional value used to control the printing of overdue messages. The
2867 default is now. It isn't the date of the invoice; that's the `_date' field.
2868 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2869 L<Time::Local> and L<Date::Parse> for conversion functions.
2871 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2878 my ($file, $lfile) = $self->print_latex(@_);
2879 my $ps = generate_ps($file);
2885 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2887 Returns an PDF invoice, as a scalar.
2889 Options can be passed as a hashref (recommended) or as a list of time, template
2890 and then any key/value pairs for any other options.
2892 I<time> an optional value used to control the printing of overdue messages. The
2893 default is now. It isn't the date of the invoice; that's the `_date' field.
2894 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2895 L<Time::Local> and L<Date::Parse> for conversion functions.
2897 I<template>, if specified, is the name of a suffix for alternate invoices.
2899 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2906 my ($file, $lfile) = $self->print_latex(@_);
2907 my $pdf = generate_pdf($file);
2913 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2915 Returns an HTML invoice, as a scalar.
2917 I<time> an optional value used to control the printing of overdue messages. The
2918 default is now. It isn't the date of the invoice; that's the `_date' field.
2919 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2920 L<Time::Local> and L<Date::Parse> for conversion functions.
2922 I<template>, if specified, is the name of a suffix for alternate invoices.
2924 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2926 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2927 when emailing the invoice as part of a multipart/related MIME email.
2935 %params = %{ shift() };
2937 $params{'time'} = shift;
2938 $params{'template'} = shift;
2939 $params{'cid'} = shift;
2942 $params{'format'} = 'html';
2944 $self->print_generic( %params );
2947 # quick subroutine for print_latex
2949 # There are ten characters that LaTeX treats as special characters, which
2950 # means that they do not simply typeset themselves:
2951 # # $ % & ~ _ ^ \ { }
2953 # TeX ignores blanks following an escaped character; if you want a blank (as
2954 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2958 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2959 $value =~ s/([<>])/\$$1\$/g;
2963 #utility methods for print_*
2965 sub _translate_old_latex_format {
2966 warn "_translate_old_latex_format called\n"
2973 if ( $line =~ /^%%Detail\s*$/ ) {
2975 push @template, q![@--!,
2976 q! foreach my $_tr_line (@detail_items) {!,
2977 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2978 q! $_tr_line->{'description'} .= !,
2979 q! "\\tabularnewline\n~~".!,
2980 q! join( "\\tabularnewline\n~~",!,
2981 q! @{$_tr_line->{'ext_description'}}!,
2985 while ( ( my $line_item_line = shift )
2986 !~ /^%%EndDetail\s*$/ ) {
2987 $line_item_line =~ s/'/\\'/g; # nice LTS
2988 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2989 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2990 push @template, " \$OUT .= '$line_item_line';";
2993 push @template, '}',
2996 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2998 push @template, '[@--',
2999 ' foreach my $_tr_line (@total_items) {';
3001 while ( ( my $total_item_line = shift )
3002 !~ /^%%EndTotalDetails\s*$/ ) {
3003 $total_item_line =~ s/'/\\'/g; # nice LTS
3004 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3005 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3006 push @template, " \$OUT .= '$total_item_line';";
3009 push @template, '}',
3013 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3014 push @template, $line;
3020 warn "$_\n" foreach @template;
3029 #check for an invoice- specific override (eventually)
3031 #check for a customer- specific override
3032 return $self->cust_main->invoice_terms
3033 if $self->cust_main->invoice_terms;
3035 #use configured default
3036 $conf->config('invoice_default_terms') || '';
3042 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3043 $duedate = $self->_date() + ( $1 * 86400 );
3050 $self->due_date ? time2str(shift, $self->due_date) : '';
3053 sub balance_due_msg {
3055 my $msg = 'Balance Due';
3056 return $msg unless $self->terms;
3057 if ( $self->due_date ) {
3058 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3059 } elsif ( $self->terms ) {
3060 $msg .= ' - '. $self->terms;
3065 sub balance_due_date {
3068 if ( $conf->exists('invoice_default_terms')
3069 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3070 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3075 =item invnum_date_pretty
3077 Returns a string with the invoice number and date, for example:
3078 "Invoice #54 (3/20/2008)"
3082 sub invnum_date_pretty {
3084 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3089 Returns a string with the date, for example: "3/20/2008"
3095 time2str('%x', $self->_date);
3098 sub _items_sections {
3101 my $summarypage = shift;
3108 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3112 my $usage = $cust_bill_pkg->usage;
3114 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3115 next if ( $display->summary && $summarypage );
3117 my $desc = $display->section;
3118 my $type = $display->type;
3120 if ( $cust_bill_pkg->pkgnum > 0 ) {
3121 $not_tax{$desc} = 1;
3124 if ( $display->post_total && !$summarypage ) {
3125 if (! $type || $type eq 'S') {
3126 $l{$desc} += $cust_bill_pkg->setup
3127 if ( $cust_bill_pkg->setup != 0 );
3131 $l{$desc} += $cust_bill_pkg->recur
3132 if ( $cust_bill_pkg->recur != 0 );
3135 if ($type && $type eq 'R') {
3136 $l{$desc} += $cust_bill_pkg->recur - $usage
3137 if ( $cust_bill_pkg->recur != 0 );
3140 if ($type && $type eq 'U') {
3141 $l{$desc} += $usage;
3145 if (! $type || $type eq 'S') {
3146 $s{$desc} += $cust_bill_pkg->setup
3147 if ( $cust_bill_pkg->setup != 0 );
3151 $s{$desc} += $cust_bill_pkg->recur
3152 if ( $cust_bill_pkg->recur != 0 );
3155 if ($type && $type eq 'R') {
3156 $s{$desc} += $cust_bill_pkg->recur - $usage
3157 if ( $cust_bill_pkg->recur != 0 );
3160 if ($type && $type eq 'U') {
3161 $s{$desc} += $usage;
3170 my %cache = map { $_->categoryname => $_ }
3171 qsearch( 'pkg_category', {disabled => 'Y'} );
3172 $cache{$_->categoryname} = $_
3173 foreach qsearch( 'pkg_category', {disabled => ''} );
3175 push @$late, map { { 'description' => &{$escape}($_),
3176 'subtotal' => $l{$_},
3179 sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
3181 map { { 'description' => &{$escape}($_),
3182 'subtotal' => $s{$_},
3183 'summarized' => $not_tax{$_} ? '' : 'Y',
3184 'tax_section' => $not_tax{$_} ? '' : 'Y',
3186 sort { $cache{$a}->weight <=> $cache{$b}->weight }
3188 ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
3197 #my @display = scalar(@_)
3199 # : qw( _items_previous _items_pkg );
3200 # #: qw( _items_pkg );
3201 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3202 my @display = qw( _items_previous _items_pkg );
3205 foreach my $display ( @display ) {
3206 push @b, $self->$display(@_);
3211 sub _items_previous {
3213 my $cust_main = $self->cust_main;
3214 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3216 foreach ( @pr_cust_bill ) {
3218 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3219 ' ('. time2str('%x',$_->_date). ')',
3220 #'pkgpart' => 'N/A',
3222 'amount' => sprintf("%.2f", $_->owed),
3228 # 'description' => 'Previous Balance',
3229 # #'pkgpart' => 'N/A',
3230 # 'pkgnum' => 'N/A',
3231 # 'amount' => sprintf("%10.2f", $pr_total ),
3232 # 'ext_description' => [ map {
3233 # "Invoice ". $_->invnum.
3234 # " (". time2str("%x",$_->_date). ") ".
3235 # sprintf("%10.2f", $_->owed)
3236 # } @pr_cust_bill ],
3243 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3244 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3248 return 0 unless $a cmp $b;
3249 return -1 if $b eq 'Tax';
3250 return 1 if $a eq 'Tax';
3251 return -1 if $b eq 'Other surcharges';
3252 return 1 if $a eq 'Other surcharges';
3258 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3259 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3262 sub _items_cust_bill_pkg {
3264 my $cust_bill_pkg = shift;
3267 my $format = $opt{format} || '';
3268 my $escape_function = $opt{escape_function} || sub { shift };
3269 my $format_function = $opt{format_function} || '';
3270 my $unsquelched = $opt{unsquelched} || '';
3271 my $section = $opt{section}->{description} if $opt{section};
3272 my $summary_page = $opt{summary_page} || '';
3275 my ($s, $r, $u) = ( undef, undef, undef );
3276 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3279 foreach ( $s, $r, $u ) {
3280 if ( $_ && !$cust_bill_pkg->hidden ) {
3281 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3282 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3288 foreach my $display ( grep { defined($section)
3289 ? $_->section eq $section
3292 grep { $_->summary || !$summary_page }
3293 $cust_bill_pkg->cust_bill_pkg_display
3297 my $type = $display->type;
3299 my $desc = $cust_bill_pkg->desc;
3300 $desc = substr($desc, 0, 50). '...'
3301 if $format eq 'latex' && length($desc) > 50;
3303 my %details_opt = ( 'format' => $format,
3304 'escape_function' => $escape_function,
3305 'format_function' => $format_function,
3308 if ( $cust_bill_pkg->pkgnum > 0 ) {
3310 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3312 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3314 my $description = $desc;
3315 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3318 push @d, map &{$escape_function}($_),
3319 $cust_pkg->h_labels_short($self->_date)
3320 unless $cust_pkg->part_pkg->hide_svc_detail
3321 || $cust_bill_pkg->hidden;
3322 push @d, $cust_bill_pkg->details(%details_opt)
3323 if $cust_bill_pkg->recur == 0;
3325 if ( $cust_bill_pkg->hidden ) {
3326 $s->{amount} += $cust_bill_pkg->setup;
3327 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3328 push @{ $s->{ext_description} }, @d;
3331 description => $description,
3332 #pkgpart => $part_pkg->pkgpart,
3333 pkgnum => $cust_bill_pkg->pkgnum,
3334 amount => $cust_bill_pkg->setup,
3335 unit_amount => $cust_bill_pkg->unitsetup,
3336 quantity => $cust_bill_pkg->quantity,
3337 ext_description => \@d,
3343 if ( $cust_bill_pkg->recur != 0 &&
3344 ( !$type || $type eq 'R' || $type eq 'U' )
3348 my $is_summary = $display->summary;
3349 my $description = ($is_summary && $type && $type eq 'U')
3350 ? "Usage charges" : $desc;
3352 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3353 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3354 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3359 #at least until cust_bill_pkg has "past" ranges in addition to
3360 #the "future" sdate/edate ones... see #3032
3361 my @dates = ( $self->_date );
3362 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3363 push @dates, $prev->sdate if $prev;
3365 push @d, map &{$escape_function}($_),
3366 $cust_pkg->h_labels_short(@dates)
3367 #$cust_bill_pkg->edate,
3368 #$cust_bill_pkg->sdate)
3369 unless $cust_pkg->part_pkg->hide_svc_detail
3370 || $cust_bill_pkg->itemdesc
3371 || $cust_bill_pkg->hidden
3372 || $is_summary && $type && $type eq 'U';
3374 push @d, $cust_bill_pkg->details(%details_opt)
3375 unless ($is_summary || $type && $type eq 'R');
3379 $amount = $cust_bill_pkg->recur;
3380 }elsif($type eq 'R') {
3381 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3382 }elsif($type eq 'U') {
3383 $amount = $cust_bill_pkg->usage;
3386 if ( !$type || $type eq 'R' ) {
3388 if ( $cust_bill_pkg->hidden ) {
3389 $r->{amount} += $amount;
3390 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3391 push @{ $r->{ext_description} }, @d;
3394 description => $description,
3395 #pkgpart => $part_pkg->pkgpart,
3396 pkgnum => $cust_bill_pkg->pkgnum,
3398 unit_amount => $cust_bill_pkg->unitrecur,
3399 quantity => $cust_bill_pkg->quantity,
3400 ext_description => \@d,
3404 } elsif ( $amount ) { # && $type eq 'U'
3406 if ( $cust_bill_pkg->hidden ) {
3407 $u->{amount} += $amount;
3408 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3409 push @{ $u->{ext_description} }, @d;
3412 description => $description,
3413 #pkgpart => $part_pkg->pkgpart,
3414 pkgnum => $cust_bill_pkg->pkgnum,
3416 unit_amount => $cust_bill_pkg->unitrecur,
3417 quantity => $cust_bill_pkg->quantity,
3418 ext_description => \@d,
3424 } # recurring or usage with recurring charge
3426 } else { #pkgnum tax or one-shot line item (??)
3428 if ( $cust_bill_pkg->setup != 0 ) {
3430 'description' => $desc,
3431 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3434 if ( $cust_bill_pkg->recur != 0 ) {
3436 'description' => "$desc (".
3437 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3438 time2str("%x", $cust_bill_pkg->edate). ')',
3439 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3449 foreach ( $s, $r, $u ) {
3451 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3452 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3461 sub _items_credits {
3462 my( $self, %opt ) = @_;
3463 my $trim_len = $opt{'trim_len'} || 60;
3467 foreach ( $self->cust_credited ) {
3469 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3471 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3472 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3473 $reason = " ($reason) " if $reason;
3476 #'description' => 'Credit ref\#'. $_->crednum.
3477 # " (". time2str("%x",$_->cust_credit->_date) .")".
3479 'description' => 'Credit applied '.
3480 time2str("%x",$_->cust_credit->_date). $reason,
3481 'amount' => sprintf("%.2f",$_->amount),
3489 sub _items_payments {
3493 #get & print payments
3494 foreach ( $self->cust_bill_pay ) {
3496 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3499 'description' => "Payment received ".
3500 time2str("%x",$_->cust_pay->_date ),
3501 'amount' => sprintf("%.2f", $_->amount )
3509 =item call_details [ OPTION => VALUE ... ]
3511 Returns an array of CSV strings representing the call details for this invoice
3512 The only option available is the boolean prepend_billed_number
3517 my ($self, %opt) = @_;
3519 my $format_function = sub { shift };
3521 if ($opt{prepend_billed_number}) {
3522 $format_function = sub {
3526 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3531 my @details = map { $_->details( 'format_function' => $format_function,
3532 'escape_function' => sub{ return() },
3536 $self->cust_bill_pkg;
3537 my $header = $details[0];
3538 ( $header, grep { $_ ne $header } @details );
3548 =item process_reprint
3552 sub process_reprint {
3553 process_re_X('print', @_);
3556 =item process_reemail
3560 sub process_reemail {
3561 process_re_X('email', @_);
3569 process_re_X('fax', @_);
3577 process_re_X('ftp', @_);
3584 sub process_respool {
3585 process_re_X('spool', @_);
3588 use Storable qw(thaw);
3592 my( $method, $job ) = ( shift, shift );
3593 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3595 my $param = thaw(decode_base64(shift));
3596 warn Dumper($param) if $DEBUG;
3607 my($method, $job, %param ) = @_;
3609 warn "re_X $method for job $job with param:\n".
3610 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3613 #some false laziness w/search/cust_bill.html
3615 my $orderby = 'ORDER BY cust_bill._date';
3617 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3619 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3621 my @cust_bill = qsearch( {
3622 #'select' => "cust_bill.*",
3623 'table' => 'cust_bill',
3624 'addl_from' => $addl_from,
3626 'extra_sql' => $extra_sql,
3627 'order_by' => $orderby,
3631 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3633 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3636 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3637 foreach my $cust_bill ( @cust_bill ) {
3638 $cust_bill->$method();
3640 if ( $job ) { #progressbar foo
3642 if ( time - $min_sec > $last ) {
3643 my $error = $job->update_statustext(
3644 int( 100 * $num / scalar(@cust_bill) )
3646 die $error if $error;
3657 =head1 CLASS METHODS
3663 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3669 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3674 Returns an SQL fragment to retreive the net amount (charged minus credited).
3680 'charged - '. $class->credited_sql;
3685 Returns an SQL fragment to retreive the amount paid against this invoice.
3691 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3692 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3697 Returns an SQL fragment to retreive the amount credited against this invoice.
3703 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3704 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3707 =item search_sql HASHREF
3709 Class method which returns an SQL WHERE fragment to search for parameters
3710 specified in HASHREF. Valid parameters are
3716 Epoch date (UNIX timestamp) setting a lower bound for _date values
3720 Epoch date (UNIX timestamp) setting an upper bound for _date values
3734 =item newest_percust
3738 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3743 my($class, $param) = @_;
3745 warn "$me search_sql called with params: \n".
3746 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3751 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3752 push @search, "cust_bill._date >= $1";
3754 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3755 push @search, "cust_bill._date < $1";
3757 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3758 push @search, "cust_bill.invnum >= $1";
3760 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3761 push @search, "cust_bill.invnum <= $1";
3763 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3764 push @search, "cust_main.agentnum = $1";
3767 push @search, '0 != '. FS::cust_bill->owed_sql
3768 if $param->{'open'};
3770 push @search, '0 != '. FS::cust_bill->net_sql
3773 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3774 if $param->{'days'};
3776 if ( $param->{'newest_percust'} ) {
3778 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3779 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3781 my @newest_where = map { my $x = $_;
3782 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3785 grep ! /^cust_main./, @search;
3786 my $newest_where = scalar(@newest_where)
3787 ? ' AND '. join(' AND ', @newest_where)
3791 push @search, "cust_bill._date = (
3792 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3793 WHERE newest_cust_bill.custnum = cust_bill.custnum
3799 my $curuser = $FS::CurrentUser::CurrentUser;
3800 if ( $curuser->username eq 'fs_queue'
3801 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3803 my $newuser = qsearchs('access_user', {
3804 'username' => $username,
3808 $curuser = $newuser;
3810 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3814 push @search, $curuser->agentnums_sql;
3816 join(' AND ', @search );
3828 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3829 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base