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 notice name instead of "Invoice", optional
779 Returns an argument list to be passed to L<FS::Misc::send_email>.
790 my $me = '[FS::cust_bill::generate_email]';
793 'from' => $args{'from'},
794 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
798 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
799 'template' => $args{'template'},
800 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
803 my $cust_main = $self->cust_main;
805 if (ref($args{'to'}) eq 'ARRAY') {
806 $return{'to'} = $args{'to'};
808 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
809 $cust_main->invoicing_list
813 if ( $conf->exists('invoice_html') ) {
815 warn "$me creating HTML/text multipart message"
818 $return{'nobody'} = 1;
820 my $alternative = build MIME::Entity
821 'Type' => 'multipart/alternative',
822 'Encoding' => '7bit',
823 'Disposition' => 'inline'
827 if ( $conf->exists('invoice_email_pdf')
828 and scalar($conf->config('invoice_email_pdf_note')) ) {
830 warn "$me using 'invoice_email_pdf_note' in multipart message"
832 $data = [ map { $_ . "\n" }
833 $conf->config('invoice_email_pdf_note')
838 warn "$me not using 'invoice_email_pdf_note' in multipart message"
840 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
841 $data = $args{'print_text'};
843 $data = [ $self->print_text(\%opt) ];
848 $alternative->attach(
849 'Type' => 'text/plain',
850 #'Encoding' => 'quoted-printable',
851 'Encoding' => '7bit',
853 'Disposition' => 'inline',
856 $args{'from'} =~ /\@([\w\.\-]+)/;
857 my $from = $1 || 'example.com';
858 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
861 my $agentnum = $cust_main->agentnum;
862 if ( defined($args{'template'}) && length($args{'template'})
863 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
866 $logo = 'logo_'. $args{'template'}. '.png';
870 my $image_data = $conf->config_binary( $logo, $agentnum);
872 my $image = build MIME::Entity
873 'Type' => 'image/png',
874 'Encoding' => 'base64',
875 'Data' => $image_data,
876 'Filename' => 'logo.png',
877 'Content-ID' => "<$content_id>",
880 $alternative->attach(
881 'Type' => 'text/html',
882 'Encoding' => 'quoted-printable',
883 'Data' => [ '<html>',
886 ' '. encode_entities($return{'subject'}),
889 ' <body bgcolor="#e8e8e8">',
890 $self->print_html({ 'cid'=>$content_id, %opt }),
894 'Disposition' => 'inline',
895 #'Filename' => 'invoice.pdf',
899 if ( $cust_main->email_csv_cdr ) {
901 push @otherparts, build MIME::Entity
902 'Type' => 'text/csv',
903 'Encoding' => '7bit',
904 'Data' => [ map { "$_\n" }
905 $self->call_details('prepend_billed_number' => 1)
907 'Disposition' => 'attachment',
908 'Filename' => 'usage-'. $self->invnum. '.csv',
913 if ( $conf->exists('invoice_email_pdf') ) {
918 # multipart/alternative
924 my $related = build MIME::Entity 'Type' => 'multipart/related',
925 'Encoding' => '7bit';
927 #false laziness w/Misc::send_email
928 $related->head->replace('Content-type',
930 '; boundary="'. $related->head->multipart_boundary. '"'.
931 '; type=multipart/alternative'
934 $related->add_part($alternative);
936 $related->add_part($image);
938 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
940 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
944 #no other attachment:
946 # multipart/alternative
951 $return{'content-type'} = 'multipart/related';
952 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
953 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
954 #$return{'disposition'} = 'inline';
960 if ( $conf->exists('invoice_email_pdf') ) {
961 warn "$me creating PDF attachment"
964 #mime parts arguments a la MIME::Entity->build().
965 $return{'mimeparts'} = [
966 { $self->mimebuild_pdf(\%opt) }
970 if ( $conf->exists('invoice_email_pdf')
971 and scalar($conf->config('invoice_email_pdf_note')) ) {
973 warn "$me using 'invoice_email_pdf_note'"
975 $return{'body'} = [ map { $_ . "\n" }
976 $conf->config('invoice_email_pdf_note')
981 warn "$me not using 'invoice_email_pdf_note'"
983 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
984 $return{'body'} = $args{'print_text'};
986 $return{'body'} = [ $self->print_text(\%opt) ];
999 Returns a list suitable for passing to MIME::Entity->build(), representing
1000 this invoice as PDF attachment.
1007 'Type' => 'application/pdf',
1008 'Encoding' => 'base64',
1009 'Data' => [ $self->print_pdf(@_) ],
1010 'Disposition' => 'attachment',
1011 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1015 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1017 Sends this invoice to the destinations configured for this customer: sends
1018 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1020 Options can be passed as a hashref (recommended) or as a list of up to
1021 four values for templatename, agentnum, invoice_from and amount.
1023 I<template>, if specified, is the name of a suffix for alternate invoices.
1025 I<agentnum>, if specified, means that this invoice will only be sent for customers
1026 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1027 single agent) or an arrayref of agentnums.
1029 I<invoice_from>, if specified, overrides the default email invoice From: address.
1031 I<amount>, if specified, only sends the invoice if the total amount owed on this
1032 invoice and all older invoices is greater than the specified amount.
1034 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1038 sub queueable_send {
1041 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1042 or die "invalid invoice number: " . $opt{invnum};
1044 my @args = ( $opt{template}, $opt{agentnum} );
1045 push @args, $opt{invoice_from}
1046 if exists($opt{invoice_from}) && $opt{invoice_from};
1048 my $error = $self->send( @args );
1049 die $error if $error;
1056 my( $template, $invoice_from, $notice_name );
1058 my $balance_over = 0;
1062 $template = $opt->{'template'} || '';
1063 if ( $agentnums = $opt->{'agentnum'} ) {
1064 $agentnums = [ $agentnums ] unless ref($agentnums);
1066 $invoice_from = $opt->{'invoice_from'};
1067 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1068 $notice_name = $opt=>{'notice_name'};
1070 $template = scalar(@_) ? shift : '';
1071 if ( scalar(@_) && $_[0] ) {
1072 $agentnums = ref($_[0]) ? shift : [ shift ];
1074 $invoice_from = shift if scalar(@_);
1075 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1078 return 'N/A' unless ! $agentnums
1079 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1082 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1084 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1085 $conf->config('invoice_from', $self->cust_main->agentnum );
1088 'template' => $template,
1089 'invoice_from' => $invoice_from,
1090 'notice_name' => ( $notice_name || 'Invoice' ),
1093 my @invoicing_list = $self->cust_main->invoicing_list;
1095 #$self->email_invoice(\%opt)
1097 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1099 #$self->print_invoice(\%opt)
1101 if grep { $_ eq 'POST' } @invoicing_list; #postal
1103 $self->fax_invoice(\%opt)
1104 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1110 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1112 Emails this invoice.
1114 Options can be passed as a hashref (recommended) or as a list of up to
1115 two values for templatename and invoice_from.
1117 I<template>, if specified, is the name of a suffix for alternate invoices.
1119 I<invoice_from>, if specified, overrides the default email invoice From: address.
1121 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1125 sub queueable_email {
1128 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1129 or die "invalid invoice number: " . $opt{invnum};
1131 my @args = ( $opt{template} );
1132 push @args, $opt{invoice_from}
1133 if exists($opt{invoice_from}) && $opt{invoice_from};
1135 my $error = $self->email( @args );
1136 die $error if $error;
1140 #sub email_invoice {
1144 my( $template, $invoice_from, $notice_name );
1147 $template = $opt->{'template'} || '';
1148 $invoice_from = $opt->{'invoice_from'};
1149 $notice_name = $opt->{'notice_name'} || 'Invoice';
1151 $template = scalar(@_) ? shift : '';
1152 $invoice_from = shift if scalar(@_);
1153 $notice_name = 'Invoice';
1156 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1157 $conf->config('invoice_from', $self->cust_main->agentnum );
1159 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1160 $self->cust_main->invoicing_list;
1162 #better to notify this person than silence
1163 @invoicing_list = ($invoice_from) unless @invoicing_list;
1165 my $subject = $self->email_subject($template);
1167 my $error = send_email(
1168 $self->generate_email(
1169 'from' => $invoice_from,
1170 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1171 'subject' => $subject,
1172 'template' => $template,
1173 'notice_name' => $notice_name,
1176 die "can't email invoice: $error\n" if $error;
1177 #die "$error\n" if $error;
1184 #my $template = scalar(@_) ? shift : '';
1187 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1190 my $cust_main = $self->cust_main;
1191 my $name = $cust_main->name;
1192 my $name_short = $cust_main->name_short;
1193 my $invoice_number = $self->invnum;
1194 my $invoice_date = $self->_date_pretty;
1196 eval qq("$subject");
1199 =item lpr_data HASHREF | [ TEMPLATE ]
1201 Returns the postscript or plaintext for this invoice as an arrayref.
1203 Options can be passed as a hashref (recommended) or as a single optional value
1206 I<template>, if specified, is the name of a suffix for alternate invoices.
1208 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1214 my( $template, $notice_name );
1217 $template = $opt->{'template'} || '';
1218 $notice_name = $opt->{'notice_name'} || 'Invoice';
1220 $template = scalar(@_) ? shift : '';
1221 $notice_name = 'Invoice';
1225 'template' => $template,
1226 'notice_name' => $notice_name,
1229 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1230 [ $self->$method( \%opt ) ];
1233 =item print HASHREF | [ TEMPLATE ]
1235 Prints this invoice.
1237 Options can be passed as a hashref (recommended) or as a single optional
1240 I<template>, if specified, is the name of a suffix for alternate invoices.
1242 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1246 #sub print_invoice {
1249 my( $template, $notice_name );
1252 $template = $opt->{'template'} || '';
1253 $notice_name = $opt->{'notice_name'} || 'Invoice';
1255 $template = scalar(@_) ? shift : '';
1256 $notice_name = 'Invoice';
1260 'template' => $template,
1261 'notice_name' => $notice_name,
1264 do_print $self->lpr_data(\%opt);
1267 =item fax_invoice HASHREF | [ TEMPLATE ]
1271 Options can be passed as a hashref (recommended) or as a single optional
1274 I<template>, if specified, is the name of a suffix for alternate invoices.
1276 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1282 my( $template, $notice_name );
1285 $template = $opt->{'template'} || '';
1286 $notice_name = $opt->{'notice_name'} || 'Invoice';
1288 $template = scalar(@_) ? shift : '';
1289 $notice_name = 'Invoice';
1292 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1293 unless $conf->exists('invoice_latex');
1295 my $dialstring = $self->cust_main->getfield('fax');
1299 'template' => $template,
1300 'notice_name' => $notice_name,
1303 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1304 'dialstring' => $dialstring,
1306 die $error if $error;
1310 =item ftp_invoice [ TEMPLATENAME ]
1312 Sends this invoice data via FTP.
1314 TEMPLATENAME is unused?
1320 my $template = scalar(@_) ? shift : '';
1323 'protocol' => 'ftp',
1324 'server' => $conf->config('cust_bill-ftpserver'),
1325 'username' => $conf->config('cust_bill-ftpusername'),
1326 'password' => $conf->config('cust_bill-ftppassword'),
1327 'dir' => $conf->config('cust_bill-ftpdir'),
1328 'format' => $conf->config('cust_bill-ftpformat'),
1332 =item spool_invoice [ TEMPLATENAME ]
1334 Spools this invoice data (see L<FS::spool_csv>)
1336 TEMPLATENAME is unused?
1342 my $template = scalar(@_) ? shift : '';
1345 'format' => $conf->config('cust_bill-spoolformat'),
1346 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1350 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1352 Like B<send>, but only sends the invoice if it is the newest open invoice for
1357 sub send_if_newest {
1362 grep { $_->owed > 0 }
1363 qsearch('cust_bill', {
1364 'custnum' => $self->custnum,
1365 #'_date' => { op=>'>', value=>$self->_date },
1366 'invnum' => { op=>'>', value=>$self->invnum },
1373 =item send_csv OPTION => VALUE, ...
1375 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1379 protocol - currently only "ftp"
1385 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1386 and YYMMDDHHMMSS is a timestamp.
1388 See L</print_csv> for a description of the output format.
1393 my($self, %opt) = @_;
1397 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1398 mkdir $spooldir, 0700 unless -d $spooldir;
1400 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1401 my $file = "$spooldir/$tracctnum.csv";
1403 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1405 open(CSV, ">$file") or die "can't open $file: $!";
1413 if ( $opt{protocol} eq 'ftp' ) {
1414 eval "use Net::FTP;";
1416 $net = Net::FTP->new($opt{server}) or die @$;
1418 die "unknown protocol: $opt{protocol}";
1421 $net->login( $opt{username}, $opt{password} )
1422 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1424 $net->binary or die "can't set binary mode";
1426 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1428 $net->put($file) or die "can't put $file: $!";
1438 Spools CSV invoice data.
1444 =item format - 'default' or 'billco'
1446 =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>).
1448 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1450 =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.
1457 my($self, %opt) = @_;
1459 my $cust_main = $self->cust_main;
1461 if ( $opt{'dest'} ) {
1462 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1463 $cust_main->invoicing_list;
1464 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1465 || ! keys %invoicing_list;
1468 if ( $opt{'balanceover'} ) {
1470 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1473 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1474 mkdir $spooldir, 0700 unless -d $spooldir;
1476 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1480 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1481 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1484 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1486 open(CSV, ">>$file") or die "can't open $file: $!";
1487 flock(CSV, LOCK_EX);
1492 if ( lc($opt{'format'}) eq 'billco' ) {
1494 flock(CSV, LOCK_UN);
1499 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1502 open(CSV,">>$file") or die "can't open $file: $!";
1503 flock(CSV, LOCK_EX);
1509 flock(CSV, LOCK_UN);
1516 =item print_csv OPTION => VALUE, ...
1518 Returns CSV data for this invoice.
1522 format - 'default' or 'billco'
1524 Returns a list consisting of two scalars. The first is a single line of CSV
1525 header information for this invoice. The second is one or more lines of CSV
1526 detail information for this invoice.
1528 If I<format> is not specified or "default", the fields of the CSV file are as
1531 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1535 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1537 B<record_type> is C<cust_bill> for the initial header line only. The
1538 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1539 fields are filled in.
1541 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1542 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1545 =item invnum - invoice number
1547 =item custnum - customer number
1549 =item _date - invoice date
1551 =item charged - total invoice amount
1553 =item first - customer first name
1555 =item last - customer first name
1557 =item company - company name
1559 =item address1 - address line 1
1561 =item address2 - address line 1
1571 =item pkg - line item description
1573 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1575 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1577 =item sdate - start date for recurring fee
1579 =item edate - end date for recurring fee
1583 If I<format> is "billco", the fields of the header CSV file are as follows:
1585 +-------------------------------------------------------------------+
1586 | FORMAT HEADER FILE |
1587 |-------------------------------------------------------------------|
1588 | Field | Description | Name | Type | Width |
1589 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1590 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1591 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1592 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1593 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1594 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1595 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1596 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1597 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1598 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1599 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1600 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1601 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1602 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1603 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1604 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1605 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1606 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1607 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1608 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1609 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1610 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1611 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1612 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1613 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1614 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1615 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1616 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1617 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1618 +-------+-------------------------------+------------+------+-------+
1620 If I<format> is "billco", the fields of the detail CSV file are as follows:
1622 FORMAT FOR DETAIL FILE
1624 Field | Description | Name | Type | Width
1625 1 | N/A-Leave Empty | RC | CHAR | 2
1626 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1627 3 | Account Number | TRACCTNUM | CHAR | 15
1628 4 | Invoice Number | TRINVOICE | CHAR | 15
1629 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1630 6 | Transaction Detail | DETAILS | CHAR | 100
1631 7 | Amount | AMT | NUM* | 9
1632 8 | Line Format Control** | LNCTRL | CHAR | 2
1633 9 | Grouping Code | GROUP | CHAR | 2
1634 10 | User Defined | ACCT CODE | CHAR | 15
1639 my($self, %opt) = @_;
1641 eval "use Text::CSV_XS";
1644 my $cust_main = $self->cust_main;
1646 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1648 if ( lc($opt{'format'}) eq 'billco' ) {
1651 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1653 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1655 my( $previous_balance, @unused ) = $self->previous; #previous balance
1657 my $pmt_cr_applied = 0;
1658 $pmt_cr_applied += $_->{'amount'}
1659 foreach ( $self->_items_payments, $self->_items_credits ) ;
1661 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1664 '', # 1 | N/A-Leave Empty CHAR 2
1665 '', # 2 | N/A-Leave Empty CHAR 15
1666 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1667 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1668 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1669 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1670 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1671 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1672 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1673 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1674 '', # 10 | Ancillary Billing Information CHAR 30
1675 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1676 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1679 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1682 $duedate, # 14 | Bill Due Date CHAR 10
1684 $previous_balance, # 15 | Previous Balance NUM* 9
1685 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1686 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1687 $totaldue, # 18 | Total Amt Due NUM* 9
1688 $totaldue, # 19 | Total Amt Due NUM* 9
1689 '', # 20 | 30 Day Aging NUM* 9
1690 '', # 21 | 60 Day Aging NUM* 9
1691 '', # 22 | 90 Day Aging NUM* 9
1692 'N', # 23 | Y/N CHAR 1
1693 '', # 24 | Remittance automation CHAR 100
1694 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1695 $self->custnum, # 26 | Customer Reference Number CHAR 15
1696 '0', # 27 | Federal Tax*** NUM* 9
1697 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1698 '0', # 29 | Other Taxes & Fees*** NUM* 9
1707 time2str("%x", $self->_date),
1708 sprintf("%.2f", $self->charged),
1709 ( map { $cust_main->getfield($_) }
1710 qw( first last company address1 address2 city state zip country ) ),
1712 ) or die "can't create csv";
1715 my $header = $csv->string. "\n";
1718 if ( lc($opt{'format'}) eq 'billco' ) {
1721 foreach my $item ( $self->_items_pkg ) {
1724 '', # 1 | N/A-Leave Empty CHAR 2
1725 '', # 2 | N/A-Leave Empty CHAR 15
1726 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1727 $self->invnum, # 4 | Invoice Number CHAR 15
1728 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1729 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1730 $item->{'amount'}, # 7 | Amount NUM* 9
1731 '', # 8 | Line Format Control** CHAR 2
1732 '', # 9 | Grouping Code CHAR 2
1733 '', # 10 | User Defined CHAR 15
1736 $detail .= $csv->string. "\n";
1742 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1744 my($pkg, $setup, $recur, $sdate, $edate);
1745 if ( $cust_bill_pkg->pkgnum ) {
1747 ($pkg, $setup, $recur, $sdate, $edate) = (
1748 $cust_bill_pkg->part_pkg->pkg,
1749 ( $cust_bill_pkg->setup != 0
1750 ? sprintf("%.2f", $cust_bill_pkg->setup )
1752 ( $cust_bill_pkg->recur != 0
1753 ? sprintf("%.2f", $cust_bill_pkg->recur )
1755 ( $cust_bill_pkg->sdate
1756 ? time2str("%x", $cust_bill_pkg->sdate)
1758 ($cust_bill_pkg->edate
1759 ?time2str("%x", $cust_bill_pkg->edate)
1763 } else { #pkgnum tax
1764 next unless $cust_bill_pkg->setup != 0;
1765 $pkg = $cust_bill_pkg->desc;
1766 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1767 ( $sdate, $edate ) = ( '', '' );
1773 ( map { '' } (1..11) ),
1774 ($pkg, $setup, $recur, $sdate, $edate)
1775 ) or die "can't create csv";
1777 $detail .= $csv->string. "\n";
1783 ( $header, $detail );
1789 Pays this invoice with a compliemntary payment. If there is an error,
1790 returns the error, otherwise returns false.
1796 my $cust_pay = new FS::cust_pay ( {
1797 'invnum' => $self->invnum,
1798 'paid' => $self->owed,
1801 'payinfo' => $self->cust_main->payinfo,
1809 Attempts to pay this invoice with a credit card payment via a
1810 Business::OnlinePayment realtime gateway. See
1811 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1812 for supported processors.
1818 $self->realtime_bop( 'CC', @_ );
1823 Attempts to pay this invoice with an electronic check (ACH) payment via a
1824 Business::OnlinePayment realtime gateway. See
1825 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1826 for supported processors.
1832 $self->realtime_bop( 'ECHECK', @_ );
1837 Attempts to pay this invoice with phone bill (LEC) payment via a
1838 Business::OnlinePayment realtime gateway. See
1839 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1840 for supported processors.
1846 $self->realtime_bop( 'LEC', @_ );
1850 my( $self, $method ) = @_;
1852 my $cust_main = $self->cust_main;
1853 my $balance = $cust_main->balance;
1854 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1855 $amount = sprintf("%.2f", $amount);
1856 return "not run (balance $balance)" unless $amount > 0;
1858 my $description = 'Internet Services';
1859 if ( $conf->exists('business-onlinepayment-description') ) {
1860 my $dtempl = $conf->config('business-onlinepayment-description');
1862 my $agent_obj = $cust_main->agent
1863 or die "can't retreive agent for $cust_main (agentnum ".
1864 $cust_main->agentnum. ")";
1865 my $agent = $agent_obj->agent;
1866 my $pkgs = join(', ',
1867 map { $_->part_pkg->pkg }
1868 grep { $_->pkgnum } $self->cust_bill_pkg
1870 $description = eval qq("$dtempl");
1873 $cust_main->realtime_bop($method, $amount,
1874 'description' => $description,
1875 'invnum' => $self->invnum,
1880 =item batch_card OPTION => VALUE...
1882 Adds a payment for this invoice to the pending credit card batch (see
1883 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1884 runs the payment using a realtime gateway.
1889 my ($self, %options) = @_;
1890 my $cust_main = $self->cust_main;
1892 $options{invnum} = $self->invnum;
1894 $cust_main->batch_card(%options);
1897 sub _agent_template {
1899 $self->cust_main->agent_template;
1902 sub _agent_invoice_from {
1904 $self->cust_main->agent_invoice_from;
1907 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1909 Returns an text invoice, as a list of lines.
1911 Options can be passed as a hashref (recommended) or as a list of time, template
1912 and then any key/value pairs for any other options.
1914 I<time>, if specified, is used to control the printing of overdue messages. The
1915 default is now. It isn't the date of the invoice; that's the `_date' field.
1916 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1917 L<Time::Local> and L<Date::Parse> for conversion functions.
1919 I<template>, if specified, is the name of a suffix for alternate invoices.
1921 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1927 my( $today, $template, %opt );
1929 %opt = %{ shift() };
1930 $today = delete($opt{'time'}) || '';
1931 $template = delete($opt{template}) || '';
1933 ( $today, $template, %opt ) = @_;
1936 my %params = ( 'format' => 'template' );
1937 $params{'time'} = $today if $today;
1938 $params{'template'} = $template if $template;
1939 $params{$_} = $opt{$_}
1940 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1942 $self->print_generic( %params );
1945 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1947 Internal method - returns a filename of a filled-in LaTeX template for this
1948 invoice (Note: add ".tex" to get the actual filename), and a filename of
1949 an associated logo (with the .eps extension included).
1951 See print_ps and print_pdf for methods that return PostScript and PDF output.
1953 Options can be passed as a hashref (recommended) or as a list of time, template
1954 and then any key/value pairs for any other options.
1956 I<time>, if specified, is used to control the printing of overdue messages. The
1957 default is now. It isn't the date of the invoice; that's the `_date' field.
1958 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1959 L<Time::Local> and L<Date::Parse> for conversion functions.
1961 I<template>, if specified, is the name of a suffix for alternate invoices.
1963 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1969 my( $today, $template, %opt );
1971 %opt = %{ shift() };
1972 $today = delete($opt{'time'}) || '';
1973 $template = delete($opt{template}) || '';
1975 ( $today, $template, %opt ) = @_;
1978 my %params = ( 'format' => 'latex' );
1979 $params{'time'} = $today if $today;
1980 $params{'template'} = $template if $template;
1981 $params{$_} = $opt{$_}
1982 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1984 $template ||= $self->_agent_template;
1986 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1987 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1991 ) or die "can't open temp file: $!\n";
1993 my $agentnum = $self->cust_main->agentnum;
1995 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1996 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1997 or die "can't write temp file: $!\n";
1999 print $lh $conf->config_binary('logo.eps', $agentnum)
2000 or die "can't write temp file: $!\n";
2003 $params{'logo_file'} = $lh->filename;
2005 my @filled_in = $self->print_generic( %params );
2007 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2011 ) or die "can't open temp file: $!\n";
2012 print $fh join('', @filled_in );
2015 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2016 return ($1, $params{'logo_file'});
2020 =item print_generic OPTION => VALUE ...
2022 Internal method - returns a filled-in template for this invoice as a scalar.
2024 See print_ps and print_pdf for methods that return PostScript and PDF output.
2026 Non optional options include
2027 format - latex, html, template
2029 Optional options include
2031 template - a value used as a suffix for a configuration template
2033 time - a value used to control the printing of overdue messages. The
2034 default is now. It isn't the date of the invoice; that's the `_date' field.
2035 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2036 L<Time::Local> and L<Date::Parse> for conversion functions.
2040 unsquelch_cdr - overrides any per customer cdr squelching when true
2042 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2046 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2047 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2050 my( $self, %params ) = @_;
2051 my $today = $params{today} ? $params{today} : time;
2052 warn "$me print_generic called on $self with suffix $params{template}\n"
2055 my $format = $params{format};
2056 die "Unknown format: $format"
2057 unless $format =~ /^(latex|html|template)$/;
2059 my $cust_main = $self->cust_main;
2060 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2061 unless $cust_main->payname
2062 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2064 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2065 'html' => [ '<%=', '%>' ],
2066 'template' => [ '{', '}' ],
2069 #create the template
2070 my $template = $params{template} ? $params{template} : $self->_agent_template;
2071 my $templatefile = "invoice_$format";
2072 $templatefile .= "_$template"
2073 if length($template);
2074 my @invoice_template = map "$_\n", $conf->config($templatefile)
2075 or die "cannot load config data $templatefile";
2078 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2079 #change this to a die when the old code is removed
2080 warn "old-style invoice template $templatefile; ".
2081 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2082 $old_latex = 'true';
2083 @invoice_template = _translate_old_latex_format(@invoice_template);
2086 my $text_template = new Text::Template(
2088 SOURCE => \@invoice_template,
2089 DELIMITERS => $delimiters{$format},
2092 $text_template->compile()
2093 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2096 # additional substitution could possibly cause breakage in existing templates
2097 my %convert_maps = (
2099 'notes' => sub { map "$_", @_ },
2100 'footer' => sub { map "$_", @_ },
2101 'smallfooter' => sub { map "$_", @_ },
2102 'returnaddress' => sub { map "$_", @_ },
2103 'coupon' => sub { map "$_", @_ },
2104 'summary' => sub { map "$_", @_ },
2110 s/%%(.*)$/<!-- $1 -->/g;
2111 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2112 s/\\begin\{enumerate\}/<ol>/g;
2114 s/\\end\{enumerate\}/<\/ol>/g;
2115 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2124 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2126 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2131 s/\\\\\*?\s*$/<BR>/;
2132 s/\\hyphenation\{[\w\s\-]+}//;
2137 'coupon' => sub { "" },
2138 'summary' => sub { "" },
2145 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2146 s/\\begin\{enumerate\}//g;
2148 s/\\end\{enumerate\}//g;
2149 s/\\textbf\{(.*)\}/$1/g;
2156 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2158 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2163 s/\\\\\*?\s*$/\n/; # dubious
2164 s/\\hyphenation\{[\w\s\-]+}//;
2168 'coupon' => sub { "" },
2169 'summary' => sub { "" },
2174 # hashes for differing output formats
2175 my %nbsps = ( 'latex' => '~',
2176 'html' => '', # '&nbps;' would be nice
2177 'template' => '', # not used
2179 my $nbsp = $nbsps{$format};
2181 my %escape_functions = ( 'latex' => \&_latex_escape,
2182 'html' => \&encode_entities,
2183 'template' => sub { shift },
2185 my $escape_function = $escape_functions{$format};
2187 my %date_formats = ( 'latex' => '%b %o, %Y',
2188 'html' => '%b %o, %Y',
2191 my $date_format = $date_formats{$format};
2193 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2195 'html' => sub { return '<b>'. shift(). '</b>'
2197 'template' => sub { shift },
2199 my $embolden_function = $embolden_functions{$format};
2202 # generate template variables
2205 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2209 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2215 $returnaddress = join("\n",
2216 $conf->config_orbase("invoice_${format}returnaddress", $template)
2219 } elsif ( grep /\S/,
2220 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2222 my $convert_map = $convert_maps{$format}{'returnaddress'};
2225 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2230 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2232 my $convert_map = $convert_maps{$format}{'returnaddress'};
2233 $returnaddress = join( "\n", &$convert_map(
2234 map { s/( {2,})/'~' x length($1)/eg;
2238 ( $conf->config('company_name', $self->cust_main->agentnum),
2239 $conf->config('company_address', $self->cust_main->agentnum),
2246 my $warning = "Couldn't find a return address; ".
2247 "do you need to set the company_address configuration value?";
2249 $returnaddress = $nbsp;
2250 #$returnaddress = $warning;
2254 my %invoice_data = (
2255 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2256 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2257 'custnum' => $cust_main->display_custnum,
2258 'invnum' => $self->invnum,
2259 'date' => time2str($date_format, $self->_date),
2260 'today' => time2str('%b %o, %Y', $today),
2261 'agent' => &$escape_function($cust_main->agent->agent),
2262 'agent_custid' => &$escape_function($cust_main->agent_custid),
2263 'payname' => &$escape_function($cust_main->payname),
2264 'company' => &$escape_function($cust_main->company),
2265 'address1' => &$escape_function($cust_main->address1),
2266 'address2' => &$escape_function($cust_main->address2),
2267 'city' => &$escape_function($cust_main->city),
2268 'state' => &$escape_function($cust_main->state),
2269 'zip' => &$escape_function($cust_main->zip),
2270 'fax' => &$escape_function($cust_main->fax),
2271 'returnaddress' => $returnaddress,
2273 'terms' => $self->terms,
2274 'template' => $template, #params{'template'},
2275 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
2276 # better hang on to conf_dir for a while
2277 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2280 'current_charges' => sprintf("%.2f", $self->charged),
2281 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2282 'ship_enable' => $conf->exists('invoice-ship_address'),
2283 'unitprices' => $conf->exists('invoice-unitprice'),
2284 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2287 $invoice_data{finance_section} = '';
2288 if ( $conf->config('finance_pkgclass') ) {
2290 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2291 $invoice_data{finance_section} = $pkg_class->categoryname;
2293 $invoice_data{finance_amount} = '0.00';
2295 my $countrydefault = $conf->config('countrydefault') || 'US';
2296 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2297 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2298 my $method = $prefix.$_;
2299 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2301 $invoice_data{'ship_country'} = ''
2302 if ( $invoice_data{'ship_country'} eq $countrydefault );
2304 $invoice_data{'cid'} = $params{'cid'}
2307 if ( $cust_main->country eq $countrydefault ) {
2308 $invoice_data{'country'} = '';
2310 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2314 $invoice_data{'address'} = \@address;
2316 $cust_main->payname.
2317 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2318 ? " (P.O. #". $cust_main->payinfo. ")"
2322 push @address, $cust_main->company
2323 if $cust_main->company;
2324 push @address, $cust_main->address1;
2325 push @address, $cust_main->address2
2326 if $cust_main->address2;
2328 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2329 push @address, $invoice_data{'country'}
2330 if $invoice_data{'country'};
2332 while (scalar(@address) < 5);
2334 $invoice_data{'logo_file'} = $params{'logo_file'}
2335 if $params{'logo_file'};
2337 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2338 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2339 #my $balance_due = $self->owed + $pr_total - $cr_total;
2340 my $balance_due = $self->owed + $pr_total;
2341 $invoice_data{'true_previous_balance'} = sprintf("%.2f", $self->previous_balance);
2342 $invoice_data{'balance_adjustments'} = sprintf("%.2f", $self->previous_balance - $self->billing_balance);
2343 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2344 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2346 my $agentnum = $self->cust_main->agentnum;
2348 my $summarypage = '';
2349 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2352 $invoice_data{'summarypage'} = $summarypage;
2354 #do variable substitution in notes, footer, smallfooter
2355 foreach my $include (qw( notes footer smallfooter coupon )) {
2357 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2360 if ( $conf->exists($inc_file, $agentnum)
2361 && length( $conf->config($inc_file, $agentnum) ) ) {
2363 @inc_src = $conf->config($inc_file, $agentnum);
2367 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2369 my $convert_map = $convert_maps{$format}{$include};
2371 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2372 s/--\@\]/$delimiters{$format}[1]/g;
2375 &$convert_map( $conf->config($inc_file, $agentnum) );
2379 my $inc_tt = new Text::Template (
2381 SOURCE => [ map "$_\n", @inc_src ],
2382 DELIMITERS => $delimiters{$format},
2383 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2385 unless ( $inc_tt->compile() ) {
2386 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2387 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2391 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2393 $invoice_data{$include} =~ s/\n+$//
2394 if ($format eq 'latex');
2397 $invoice_data{'po_line'} =
2398 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2399 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2402 my %money_chars = ( 'latex' => '',
2403 'html' => $conf->config('money_char') || '$',
2406 my $money_char = $money_chars{$format};
2408 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2409 'html' => $conf->config('money_char') || '$',
2412 my $other_money_char = $other_money_chars{$format};
2413 $invoice_data{'dollar'} = $other_money_char;
2415 my @detail_items = ();
2416 my @total_items = ();
2420 $invoice_data{'detail_items'} = \@detail_items;
2421 $invoice_data{'total_items'} = \@total_items;
2422 $invoice_data{'buf'} = \@buf;
2423 $invoice_data{'sections'} = \@sections;
2425 my $previous_section = { 'description' => 'Previous Charges',
2426 'subtotal' => $other_money_char.
2427 sprintf('%.2f', $pr_total),
2428 'summarized' => $summarypage ? 'Y' : '',
2432 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2433 'subtotal' => $taxtotal, # adjusted below
2434 'summarized' => $summarypage ? 'Y' : '',
2437 my $adjusttotal = 0;
2438 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2439 'subtotal' => 0, # adjusted below
2440 'summarized' => $summarypage ? 'Y' : '',
2443 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2444 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2445 my $late_sections = [];
2446 if ( $multisection ) {
2448 $self->_items_sections( $late_sections, $summarypage, $escape_function );
2450 push @sections, { 'description' => '', 'subtotal' => '' };
2453 unless ( $conf->exists('disable_previous_balance')
2454 || $conf->exists('previous_balance-summary_only')
2458 foreach my $line_item ( $self->_items_previous ) {
2461 ext_description => [],
2463 $detail->{'ref'} = $line_item->{'pkgnum'};
2464 $detail->{'quantity'} = 1;
2465 $detail->{'section'} = $previous_section;
2466 $detail->{'description'} = &$escape_function($line_item->{'description'});
2467 if ( exists $line_item->{'ext_description'} ) {
2468 @{$detail->{'ext_description'}} = map {
2469 &$escape_function($_);
2470 } @{$line_item->{'ext_description'}};
2472 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2473 $line_item->{'amount'};
2474 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2476 push @detail_items, $detail;
2477 push @buf, [ $detail->{'description'},
2478 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2484 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2485 push @buf, ['','-----------'];
2486 push @buf, [ 'Total Previous Balance',
2487 $money_char. sprintf("%10.2f", $pr_total) ];
2491 foreach my $section (@sections, @$late_sections) {
2493 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2494 if ( $invoice_data{finance_section} &&
2495 $section->{'description'} eq $invoice_data{finance_section} );
2497 $section->{'subtotal'} = $other_money_char.
2498 sprintf('%.2f', $section->{'subtotal'})
2501 if ( $section->{'description'} ) {
2502 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2508 $options{'section'} = $section if $multisection;
2509 $options{'format'} = $format;
2510 $options{'escape_function'} = $escape_function;
2511 $options{'format_function'} = sub { () } unless $unsquelched;
2512 $options{'unsquelched'} = $unsquelched;
2513 $options{'summary_page'} = $summarypage;
2515 foreach my $line_item ( $self->_items_pkg(%options) ) {
2517 ext_description => [],
2519 $detail->{'ref'} = $line_item->{'pkgnum'};
2520 $detail->{'quantity'} = $line_item->{'quantity'};
2521 $detail->{'section'} = $section;
2522 $detail->{'description'} = &$escape_function($line_item->{'description'});
2523 if ( exists $line_item->{'ext_description'} ) {
2524 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2526 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2527 $line_item->{'amount'};
2528 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2529 $line_item->{'unit_amount'};
2530 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2532 push @detail_items, $detail;
2533 push @buf, ( [ $detail->{'description'},
2534 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2536 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2540 if ( $section->{'description'} ) {
2541 push @buf, ( ['','-----------'],
2542 [ $section->{'description'}. ' sub-total',
2543 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2552 $invoice_data{current_less_finance} =
2553 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2555 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2556 unshift @sections, $previous_section if $pr_total;
2559 foreach my $tax ( $self->_items_tax ) {
2561 $taxtotal += $tax->{'amount'};
2563 my $description = &$escape_function( $tax->{'description'} );
2564 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2566 if ( $multisection ) {
2568 my $money = $old_latex ? '' : $money_char;
2569 push @detail_items, {
2570 ext_description => [],
2573 description => $description,
2574 amount => $money. $amount,
2576 section => $tax_section,
2581 push @total_items, {
2582 'total_item' => $description,
2583 'total_amount' => $other_money_char. $amount,
2588 push @buf,[ $description,
2589 $money_char. $amount,
2596 $total->{'total_item'} = 'Sub-total';
2597 $total->{'total_amount'} =
2598 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2600 if ( $multisection ) {
2601 $tax_section->{'subtotal'} = $other_money_char.
2602 sprintf('%.2f', $taxtotal);
2603 $tax_section->{'pretotal'} = 'New charges sub-total '.
2604 $total->{'total_amount'};
2605 push @sections, $tax_section if $taxtotal;
2607 unshift @total_items, $total;
2610 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2612 push @buf,['','-----------'];
2613 push @buf,[( $conf->exists('disable_previous_balance')
2615 : 'Total New Charges'
2617 $money_char. sprintf("%10.2f",$self->charged) ];
2622 $total->{'total_item'} = &$embolden_function('Total');
2623 $total->{'total_amount'} =
2624 &$embolden_function(
2627 $self->charged + ( $conf->exists('disable_previous_balance')
2633 if ( $multisection ) {
2634 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2635 sprintf('%.2f', $self->charged );
2637 push @total_items, $total;
2639 push @buf,['','-----------'];
2640 push @buf,['Total Charges',
2642 sprintf( '%10.2f', $self->charged +
2643 ( $conf->exists('disable_previous_balance')
2652 unless ( $conf->exists('disable_previous_balance') ) {
2653 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2656 my $credittotal = 0;
2657 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2660 $total->{'total_item'} = &$escape_function($credit->{'description'});
2661 $credittotal += $credit->{'amount'};
2662 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2663 $adjusttotal += $credit->{'amount'};
2664 if ( $multisection ) {
2665 my $money = $old_latex ? '' : $money_char;
2666 push @detail_items, {
2667 ext_description => [],
2670 description => &$escape_function($credit->{'description'}),
2671 amount => $money. $credit->{'amount'},
2673 section => $adjust_section,
2676 push @total_items, $total;
2680 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2683 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2684 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2688 my $paymenttotal = 0;
2689 foreach my $payment ( $self->_items_payments ) {
2691 $total->{'total_item'} = &$escape_function($payment->{'description'});
2692 $paymenttotal += $payment->{'amount'};
2693 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2694 $adjusttotal += $payment->{'amount'};
2695 if ( $multisection ) {
2696 my $money = $old_latex ? '' : $money_char;
2697 push @detail_items, {
2698 ext_description => [],
2701 description => &$escape_function($payment->{'description'}),
2702 amount => $money. $payment->{'amount'},
2704 section => $adjust_section,
2707 push @total_items, $total;
2709 push @buf, [ $payment->{'description'},
2710 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2713 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2715 if ( $multisection ) {
2716 $adjust_section->{'subtotal'} = $other_money_char.
2717 sprintf('%.2f', $adjusttotal);
2718 push @sections, $adjust_section;
2723 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2724 $total->{'total_amount'} =
2725 &$embolden_function(
2726 $other_money_char. sprintf('%.2f', $summarypage
2728 $self->billing_balance
2729 : $self->owed + $pr_total
2732 if ( $multisection ) {
2733 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2734 $total->{'total_amount'};
2736 push @total_items, $total;
2738 push @buf,['','-----------'];
2739 push @buf,[$self->balance_due_msg, $money_char.
2740 sprintf("%10.2f", $balance_due ) ];
2744 if ( $multisection ) {
2745 push @sections, @$late_sections
2749 my @includelist = ();
2750 push @includelist, 'summary' if $summarypage;
2751 foreach my $include ( @includelist ) {
2753 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2756 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2758 @inc_src = $conf->config($inc_file, $agentnum);
2762 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2764 my $convert_map = $convert_maps{$format}{$include};
2766 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2767 s/--\@\]/$delimiters{$format}[1]/g;
2770 &$convert_map( $conf->config($inc_file, $agentnum) );
2774 my $inc_tt = new Text::Template (
2776 SOURCE => [ map "$_\n", @inc_src ],
2777 DELIMITERS => $delimiters{$format},
2778 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2780 unless ( $inc_tt->compile() ) {
2781 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2782 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2786 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2788 $invoice_data{$include} =~ s/\n+$//
2789 if ($format eq 'latex');
2794 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2795 /invoice_lines\((\d*)\)/;
2796 $invoice_lines += $1 || scalar(@buf);
2799 die "no invoice_lines() functions in template?"
2800 if ( $format eq 'template' && !$wasfunc );
2802 if ($format eq 'template') {
2804 if ( $invoice_lines ) {
2805 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2806 $invoice_data{'total_pages'}++
2807 if scalar(@buf) % $invoice_lines;
2810 #setup subroutine for the template
2811 sub FS::cust_bill::_template::invoice_lines {
2812 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2814 scalar(@FS::cust_bill::_template::buf)
2815 ? shift @FS::cust_bill::_template::buf
2824 push @collect, split("\n",
2825 $text_template->fill_in( HASH => \%invoice_data,
2826 PACKAGE => 'FS::cust_bill::_template'
2829 $FS::cust_bill::_template::page++;
2831 map "$_\n", @collect;
2833 warn "filling in template for invoice ". $self->invnum. "\n"
2835 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2838 $text_template->fill_in(HASH => \%invoice_data);
2842 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2844 Returns an postscript invoice, as a scalar.
2846 Options can be passed as a hashref (recommended) or as a list of time, template
2847 and then any key/value pairs for any other options.
2849 I<time> an optional value used to control the printing of overdue messages. The
2850 default is now. It isn't the date of the invoice; that's the `_date' field.
2851 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2852 L<Time::Local> and L<Date::Parse> for conversion functions.
2854 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2861 my ($file, $lfile) = $self->print_latex(@_);
2862 my $ps = generate_ps($file);
2868 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2870 Returns an PDF invoice, as a scalar.
2872 Options can be passed as a hashref (recommended) or as a list of time, template
2873 and then any key/value pairs for any other options.
2875 I<time> an optional value used to control the printing of overdue messages. The
2876 default is now. It isn't the date of the invoice; that's the `_date' field.
2877 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2878 L<Time::Local> and L<Date::Parse> for conversion functions.
2880 I<template>, if specified, is the name of a suffix for alternate invoices.
2882 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2889 my ($file, $lfile) = $self->print_latex(@_);
2890 my $pdf = generate_pdf($file);
2896 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2898 Returns an HTML invoice, as a scalar.
2900 I<time> an optional value used to control the printing of overdue messages. The
2901 default is now. It isn't the date of the invoice; that's the `_date' field.
2902 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2903 L<Time::Local> and L<Date::Parse> for conversion functions.
2905 I<template>, if specified, is the name of a suffix for alternate invoices.
2907 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2909 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2910 when emailing the invoice as part of a multipart/related MIME email.
2918 %params = %{ shift() };
2920 $params{'time'} = shift;
2921 $params{'template'} = shift;
2922 $params{'cid'} = shift;
2925 $params{'format'} = 'html';
2927 $self->print_generic( %params );
2930 # quick subroutine for print_latex
2932 # There are ten characters that LaTeX treats as special characters, which
2933 # means that they do not simply typeset themselves:
2934 # # $ % & ~ _ ^ \ { }
2936 # TeX ignores blanks following an escaped character; if you want a blank (as
2937 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2941 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2942 $value =~ s/([<>])/\$$1\$/g;
2946 #utility methods for print_*
2948 sub _translate_old_latex_format {
2949 warn "_translate_old_latex_format called\n"
2956 if ( $line =~ /^%%Detail\s*$/ ) {
2958 push @template, q![@--!,
2959 q! foreach my $_tr_line (@detail_items) {!,
2960 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2961 q! $_tr_line->{'description'} .= !,
2962 q! "\\tabularnewline\n~~".!,
2963 q! join( "\\tabularnewline\n~~",!,
2964 q! @{$_tr_line->{'ext_description'}}!,
2968 while ( ( my $line_item_line = shift )
2969 !~ /^%%EndDetail\s*$/ ) {
2970 $line_item_line =~ s/'/\\'/g; # nice LTS
2971 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2972 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2973 push @template, " \$OUT .= '$line_item_line';";
2976 push @template, '}',
2979 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2981 push @template, '[@--',
2982 ' foreach my $_tr_line (@total_items) {';
2984 while ( ( my $total_item_line = shift )
2985 !~ /^%%EndTotalDetails\s*$/ ) {
2986 $total_item_line =~ s/'/\\'/g; # nice LTS
2987 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2988 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2989 push @template, " \$OUT .= '$total_item_line';";
2992 push @template, '}',
2996 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2997 push @template, $line;
3003 warn "$_\n" foreach @template;
3012 #check for an invoice- specific override (eventually)
3014 #check for a customer- specific override
3015 return $self->cust_main->invoice_terms
3016 if $self->cust_main->invoice_terms;
3018 #use configured default
3019 $conf->config('invoice_default_terms') || '';
3025 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3026 $duedate = $self->_date() + ( $1 * 86400 );
3033 $self->due_date ? time2str(shift, $self->due_date) : '';
3036 sub balance_due_msg {
3038 my $msg = 'Balance Due';
3039 return $msg unless $self->terms;
3040 if ( $self->due_date ) {
3041 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3042 } elsif ( $self->terms ) {
3043 $msg .= ' - '. $self->terms;
3048 sub balance_due_date {
3051 if ( $conf->exists('invoice_default_terms')
3052 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3053 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3058 =item invnum_date_pretty
3060 Returns a string with the invoice number and date, for example:
3061 "Invoice #54 (3/20/2008)"
3065 sub invnum_date_pretty {
3067 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3072 Returns a string with the date, for example: "3/20/2008"
3078 time2str('%x', $self->_date);
3081 sub _items_sections {
3084 my $summarypage = shift;
3091 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3095 my $usage = $cust_bill_pkg->usage;
3097 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3098 next if ( $display->summary && $summarypage );
3100 my $desc = $display->section;
3101 my $type = $display->type;
3103 if ( $cust_bill_pkg->pkgnum > 0 ) {
3104 $not_tax{$desc} = 1;
3107 if ( $display->post_total && !$summarypage ) {
3108 if (! $type || $type eq 'S') {
3109 $l{$desc} += $cust_bill_pkg->setup
3110 if ( $cust_bill_pkg->setup != 0 );
3114 $l{$desc} += $cust_bill_pkg->recur
3115 if ( $cust_bill_pkg->recur != 0 );
3118 if ($type && $type eq 'R') {
3119 $l{$desc} += $cust_bill_pkg->recur - $usage
3120 if ( $cust_bill_pkg->recur != 0 );
3123 if ($type && $type eq 'U') {
3124 $l{$desc} += $usage;
3128 if (! $type || $type eq 'S') {
3129 $s{$desc} += $cust_bill_pkg->setup
3130 if ( $cust_bill_pkg->setup != 0 );
3134 $s{$desc} += $cust_bill_pkg->recur
3135 if ( $cust_bill_pkg->recur != 0 );
3138 if ($type && $type eq 'R') {
3139 $s{$desc} += $cust_bill_pkg->recur - $usage
3140 if ( $cust_bill_pkg->recur != 0 );
3143 if ($type && $type eq 'U') {
3144 $s{$desc} += $usage;
3153 my %cache = map { $_->categoryname => $_ }
3154 qsearch( 'pkg_category', {disabled => 'Y'} );
3155 $cache{$_->categoryname} = $_
3156 foreach qsearch( 'pkg_category', {disabled => ''} );
3158 push @$late, map { { 'description' => &{$escape}($_),
3159 'subtotal' => $l{$_},
3162 sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
3164 map { { 'description' => &{$escape}($_),
3165 'subtotal' => $s{$_},
3166 'summarized' => $not_tax{$_} ? '' : 'Y',
3167 'tax_section' => $not_tax{$_} ? '' : 'Y',
3169 sort { $cache{$a}->weight <=> $cache{$b}->weight }
3171 ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
3180 #my @display = scalar(@_)
3182 # : qw( _items_previous _items_pkg );
3183 # #: qw( _items_pkg );
3184 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3185 my @display = qw( _items_previous _items_pkg );
3188 foreach my $display ( @display ) {
3189 push @b, $self->$display(@_);
3194 sub _items_previous {
3196 my $cust_main = $self->cust_main;
3197 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3199 foreach ( @pr_cust_bill ) {
3201 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3202 ' ('. time2str('%x',$_->_date). ')',
3203 #'pkgpart' => 'N/A',
3205 'amount' => sprintf("%.2f", $_->owed),
3211 # 'description' => 'Previous Balance',
3212 # #'pkgpart' => 'N/A',
3213 # 'pkgnum' => 'N/A',
3214 # 'amount' => sprintf("%10.2f", $pr_total ),
3215 # 'ext_description' => [ map {
3216 # "Invoice ". $_->invnum.
3217 # " (". time2str("%x",$_->_date). ") ".
3218 # sprintf("%10.2f", $_->owed)
3219 # } @pr_cust_bill ],
3226 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3227 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3231 return 0 unless $a cmp $b;
3232 return -1 if $b eq 'Tax';
3233 return 1 if $a eq 'Tax';
3234 return -1 if $b eq 'Other surcharges';
3235 return 1 if $a eq 'Other surcharges';
3241 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3242 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3245 sub _items_cust_bill_pkg {
3247 my $cust_bill_pkg = shift;
3250 my $format = $opt{format} || '';
3251 my $escape_function = $opt{escape_function} || sub { shift };
3252 my $format_function = $opt{format_function} || '';
3253 my $unsquelched = $opt{unsquelched} || '';
3254 my $section = $opt{section}->{description} if $opt{section};
3255 my $summary_page = $opt{summary_page} || '';
3258 my ($s, $r, $u) = ( undef, undef, undef );
3259 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3262 foreach ( $s, $r, $u ) {
3263 if ( $_ && !$cust_bill_pkg->hidden ) {
3264 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3265 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3271 foreach my $display ( grep { defined($section)
3272 ? $_->section eq $section
3275 grep { $_->summary || !$summary_page }
3276 $cust_bill_pkg->cust_bill_pkg_display
3280 my $type = $display->type;
3282 my $desc = $cust_bill_pkg->desc;
3283 $desc = substr($desc, 0, 50). '...'
3284 if $format eq 'latex' && length($desc) > 50;
3286 my %details_opt = ( 'format' => $format,
3287 'escape_function' => $escape_function,
3288 'format_function' => $format_function,
3291 if ( $cust_bill_pkg->pkgnum > 0 ) {
3293 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3295 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3297 my $description = $desc;
3298 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3301 push @d, map &{$escape_function}($_),
3302 $cust_pkg->h_labels_short($self->_date)
3303 unless $cust_pkg->part_pkg->hide_svc_detail
3304 || $cust_bill_pkg->hidden;
3305 push @d, $cust_bill_pkg->details(%details_opt)
3306 if $cust_bill_pkg->recur == 0;
3308 if ( $cust_bill_pkg->hidden ) {
3309 $s->{amount} += $cust_bill_pkg->setup;
3310 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3311 push @{ $s->{ext_description} }, @d;
3314 description => $description,
3315 #pkgpart => $part_pkg->pkgpart,
3316 pkgnum => $cust_bill_pkg->pkgnum,
3317 amount => $cust_bill_pkg->setup,
3318 unit_amount => $cust_bill_pkg->unitsetup,
3319 quantity => $cust_bill_pkg->quantity,
3320 ext_description => \@d,
3326 if ( $cust_bill_pkg->recur != 0 &&
3327 ( !$type || $type eq 'R' || $type eq 'U' )
3331 my $is_summary = $display->summary;
3332 my $description = ($is_summary && $type && $type eq 'U')
3333 ? "Usage charges" : $desc;
3335 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3336 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3337 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3342 #at least until cust_bill_pkg has "past" ranges in addition to
3343 #the "future" sdate/edate ones... see #3032
3344 my @dates = ( $self->_date );
3345 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3346 push @dates, $prev->sdate if $prev;
3348 push @d, map &{$escape_function}($_),
3349 $cust_pkg->h_labels_short(@dates)
3350 #$cust_bill_pkg->edate,
3351 #$cust_bill_pkg->sdate)
3352 unless $cust_pkg->part_pkg->hide_svc_detail
3353 || $cust_bill_pkg->itemdesc
3354 || $cust_bill_pkg->hidden
3355 || $is_summary && $type && $type eq 'U';
3357 push @d, $cust_bill_pkg->details(%details_opt)
3358 unless ($is_summary || $type && $type eq 'R');
3362 $amount = $cust_bill_pkg->recur;
3363 }elsif($type eq 'R') {
3364 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3365 }elsif($type eq 'U') {
3366 $amount = $cust_bill_pkg->usage;
3369 if ( !$type || $type eq 'R' ) {
3371 if ( $cust_bill_pkg->hidden ) {
3372 $r->{amount} += $amount;
3373 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3374 push @{ $r->{ext_description} }, @d;
3377 description => $description,
3378 #pkgpart => $part_pkg->pkgpart,
3379 pkgnum => $cust_bill_pkg->pkgnum,
3381 unit_amount => $cust_bill_pkg->unitrecur,
3382 quantity => $cust_bill_pkg->quantity,
3383 ext_description => \@d,
3387 } elsif ( $amount ) { # && $type eq 'U'
3389 if ( $cust_bill_pkg->hidden ) {
3390 $u->{amount} += $amount;
3391 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3392 push @{ $u->{ext_description} }, @d;
3395 description => $description,
3396 #pkgpart => $part_pkg->pkgpart,
3397 pkgnum => $cust_bill_pkg->pkgnum,
3399 unit_amount => $cust_bill_pkg->unitrecur,
3400 quantity => $cust_bill_pkg->quantity,
3401 ext_description => \@d,
3407 } # recurring or usage with recurring charge
3409 } else { #pkgnum tax or one-shot line item (??)
3411 if ( $cust_bill_pkg->setup != 0 ) {
3413 'description' => $desc,
3414 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3417 if ( $cust_bill_pkg->recur != 0 ) {
3419 'description' => "$desc (".
3420 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3421 time2str("%x", $cust_bill_pkg->edate). ')',
3422 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3432 foreach ( $s, $r, $u ) {
3434 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3435 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3444 sub _items_credits {
3445 my( $self, %opt ) = @_;
3446 my $trim_len = $opt{'trim_len'} || 60;
3450 foreach ( $self->cust_credited ) {
3452 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3454 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3455 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3456 $reason = " ($reason) " if $reason;
3459 #'description' => 'Credit ref\#'. $_->crednum.
3460 # " (". time2str("%x",$_->cust_credit->_date) .")".
3462 'description' => 'Credit applied '.
3463 time2str("%x",$_->cust_credit->_date). $reason,
3464 'amount' => sprintf("%.2f",$_->amount),
3472 sub _items_payments {
3476 #get & print payments
3477 foreach ( $self->cust_bill_pay ) {
3479 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3482 'description' => "Payment received ".
3483 time2str("%x",$_->cust_pay->_date ),
3484 'amount' => sprintf("%.2f", $_->amount )
3492 =item call_details [ OPTION => VALUE ... ]
3494 Returns an array of CSV strings representing the call details for this invoice
3495 The only option available is the boolean prepend_billed_number
3500 my ($self, %opt) = @_;
3502 my $format_function = sub { shift };
3504 if ($opt{prepend_billed_number}) {
3505 $format_function = sub {
3509 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3514 my @details = map { $_->details( 'format_function' => $format_function,
3515 'escape_function' => sub{ return() },
3519 $self->cust_bill_pkg;
3520 my $header = $details[0];
3521 ( $header, grep { $_ ne $header } @details );
3531 =item process_reprint
3535 sub process_reprint {
3536 process_re_X('print', @_);
3539 =item process_reemail
3543 sub process_reemail {
3544 process_re_X('email', @_);
3552 process_re_X('fax', @_);
3560 process_re_X('ftp', @_);
3567 sub process_respool {
3568 process_re_X('spool', @_);
3571 use Storable qw(thaw);
3575 my( $method, $job ) = ( shift, shift );
3576 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3578 my $param = thaw(decode_base64(shift));
3579 warn Dumper($param) if $DEBUG;
3590 my($method, $job, %param ) = @_;
3592 warn "re_X $method for job $job with param:\n".
3593 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3596 #some false laziness w/search/cust_bill.html
3598 my $orderby = 'ORDER BY cust_bill._date';
3600 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3602 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3604 my @cust_bill = qsearch( {
3605 #'select' => "cust_bill.*",
3606 'table' => 'cust_bill',
3607 'addl_from' => $addl_from,
3609 'extra_sql' => $extra_sql,
3610 'order_by' => $orderby,
3614 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3616 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3619 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3620 foreach my $cust_bill ( @cust_bill ) {
3621 $cust_bill->$method();
3623 if ( $job ) { #progressbar foo
3625 if ( time - $min_sec > $last ) {
3626 my $error = $job->update_statustext(
3627 int( 100 * $num / scalar(@cust_bill) )
3629 die $error if $error;
3640 =head1 CLASS METHODS
3646 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3652 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3657 Returns an SQL fragment to retreive the net amount (charged minus credited).
3663 'charged - '. $class->credited_sql;
3668 Returns an SQL fragment to retreive the amount paid against this invoice.
3674 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3675 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3680 Returns an SQL fragment to retreive the amount credited against this invoice.
3686 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3687 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3690 =item search_sql HASHREF
3692 Class method which returns an SQL WHERE fragment to search for parameters
3693 specified in HASHREF. Valid parameters are
3699 Epoch date (UNIX timestamp) setting a lower bound for _date values
3703 Epoch date (UNIX timestamp) setting an upper bound for _date values
3717 =item newest_percust
3721 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3726 my($class, $param) = @_;
3728 warn "$me search_sql called with params: \n".
3729 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3734 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3735 push @search, "cust_bill._date >= $1";
3737 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3738 push @search, "cust_bill._date < $1";
3740 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3741 push @search, "cust_bill.invnum >= $1";
3743 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3744 push @search, "cust_bill.invnum <= $1";
3746 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3747 push @search, "cust_main.agentnum = $1";
3750 push @search, '0 != '. FS::cust_bill->owed_sql
3751 if $param->{'open'};
3753 push @search, '0 != '. FS::cust_bill->net_sql
3756 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3757 if $param->{'days'};
3759 if ( $param->{'newest_percust'} ) {
3761 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3762 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3764 my @newest_where = map { my $x = $_;
3765 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3768 grep ! /^cust_main./, @search;
3769 my $newest_where = scalar(@newest_where)
3770 ? ' AND '. join(' AND ', @newest_where)
3774 push @search, "cust_bill._date = (
3775 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3776 WHERE newest_cust_bill.custnum = cust_bill.custnum
3782 my $curuser = $FS::CurrentUser::CurrentUser;
3783 if ( $curuser->username eq 'fs_queue'
3784 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3786 my $newuser = qsearchs('access_user', {
3787 'username' => $username,
3791 $curuser = $newuser;
3793 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3797 push @search, $curuser->agentnums_sql;
3799 join(' AND ', @search );
3811 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3812 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base