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 = (
2257 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2258 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2259 'returnaddress' => $returnaddress,
2260 'agent' => &$escape_function($cust_main->agent->agent),
2263 'invnum' => $self->invnum,
2264 'date' => time2str($date_format, $self->_date),
2265 'today' => time2str('%b %o, %Y', $today),
2266 'terms' => $self->terms,
2267 'template' => $template, #params{'template'},
2268 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2269 'current_charges' => sprintf("%.2f", $self->charged),
2270 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2273 'custnum' => $cust_main->display_custnum,
2274 'agent_custid' => &$escape_function($cust_main->agent_custid),
2275 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2276 payname company address1 address2 city state zip fax
2280 'ship_enable' => $conf->exists('invoice-ship_address'),
2281 'unitprices' => $conf->exists('invoice-unitprice'),
2282 'smallernotes' => $conf->exists('invoice-smallernotes'),
2283 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2285 # better hang on to conf_dir for a while (for old templates)
2286 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2288 #these are only used when doing paged plaintext
2294 $invoice_data{finance_section} = '';
2295 if ( $conf->config('finance_pkgclass') ) {
2297 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2298 $invoice_data{finance_section} = $pkg_class->categoryname;
2300 $invoice_data{finance_amount} = '0.00';
2302 my $countrydefault = $conf->config('countrydefault') || 'US';
2303 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2304 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2305 my $method = $prefix.$_;
2306 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2308 $invoice_data{'ship_country'} = ''
2309 if ( $invoice_data{'ship_country'} eq $countrydefault );
2311 $invoice_data{'cid'} = $params{'cid'}
2314 if ( $cust_main->country eq $countrydefault ) {
2315 $invoice_data{'country'} = '';
2317 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2321 $invoice_data{'address'} = \@address;
2323 $cust_main->payname.
2324 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2325 ? " (P.O. #". $cust_main->payinfo. ")"
2329 push @address, $cust_main->company
2330 if $cust_main->company;
2331 push @address, $cust_main->address1;
2332 push @address, $cust_main->address2
2333 if $cust_main->address2;
2335 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2336 push @address, $invoice_data{'country'}
2337 if $invoice_data{'country'};
2339 while (scalar(@address) < 5);
2341 $invoice_data{'logo_file'} = $params{'logo_file'}
2342 if $params{'logo_file'};
2344 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2345 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2346 #my $balance_due = $self->owed + $pr_total - $cr_total;
2347 my $balance_due = $self->owed + $pr_total;
2348 $invoice_data{'true_previous_balance'} = sprintf("%.2f", $self->previous_balance);
2349 $invoice_data{'balance_adjustments'} = sprintf("%.2f", $self->previous_balance - $self->billing_balance);
2350 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2351 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2353 my $agentnum = $self->cust_main->agentnum;
2355 my $summarypage = '';
2356 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2359 $invoice_data{'summarypage'} = $summarypage;
2361 #do variable substitution in notes, footer, smallfooter
2362 foreach my $include (qw( notes footer smallfooter coupon )) {
2364 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2367 if ( $conf->exists($inc_file, $agentnum)
2368 && length( $conf->config($inc_file, $agentnum) ) ) {
2370 @inc_src = $conf->config($inc_file, $agentnum);
2374 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2376 my $convert_map = $convert_maps{$format}{$include};
2378 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2379 s/--\@\]/$delimiters{$format}[1]/g;
2382 &$convert_map( $conf->config($inc_file, $agentnum) );
2386 my $inc_tt = new Text::Template (
2388 SOURCE => [ map "$_\n", @inc_src ],
2389 DELIMITERS => $delimiters{$format},
2390 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2392 unless ( $inc_tt->compile() ) {
2393 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2394 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2398 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2400 $invoice_data{$include} =~ s/\n+$//
2401 if ($format eq 'latex');
2404 $invoice_data{'po_line'} =
2405 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2406 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2409 my %money_chars = ( 'latex' => '',
2410 'html' => $conf->config('money_char') || '$',
2413 my $money_char = $money_chars{$format};
2415 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2416 'html' => $conf->config('money_char') || '$',
2419 my $other_money_char = $other_money_chars{$format};
2420 $invoice_data{'dollar'} = $other_money_char;
2422 my @detail_items = ();
2423 my @total_items = ();
2427 $invoice_data{'detail_items'} = \@detail_items;
2428 $invoice_data{'total_items'} = \@total_items;
2429 $invoice_data{'buf'} = \@buf;
2430 $invoice_data{'sections'} = \@sections;
2432 my $previous_section = { 'description' => 'Previous Charges',
2433 'subtotal' => $other_money_char.
2434 sprintf('%.2f', $pr_total),
2435 'summarized' => $summarypage ? 'Y' : '',
2439 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2440 'subtotal' => $taxtotal, # adjusted below
2441 'summarized' => $summarypage ? 'Y' : '',
2444 my $adjusttotal = 0;
2445 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2446 'subtotal' => 0, # adjusted below
2447 'summarized' => $summarypage ? 'Y' : '',
2450 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2451 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2452 my $late_sections = [];
2453 if ( $multisection ) {
2455 $self->_items_sections( $late_sections, $summarypage, $escape_function );
2457 push @sections, { 'description' => '', 'subtotal' => '' };
2460 unless ( $conf->exists('disable_previous_balance')
2461 || $conf->exists('previous_balance-summary_only')
2465 foreach my $line_item ( $self->_items_previous ) {
2468 ext_description => [],
2470 $detail->{'ref'} = $line_item->{'pkgnum'};
2471 $detail->{'quantity'} = 1;
2472 $detail->{'section'} = $previous_section;
2473 $detail->{'description'} = &$escape_function($line_item->{'description'});
2474 if ( exists $line_item->{'ext_description'} ) {
2475 @{$detail->{'ext_description'}} = map {
2476 &$escape_function($_);
2477 } @{$line_item->{'ext_description'}};
2479 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2480 $line_item->{'amount'};
2481 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2483 push @detail_items, $detail;
2484 push @buf, [ $detail->{'description'},
2485 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2491 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2492 push @buf, ['','-----------'];
2493 push @buf, [ 'Total Previous Balance',
2494 $money_char. sprintf("%10.2f", $pr_total) ];
2498 foreach my $section (@sections, @$late_sections) {
2500 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2501 if ( $invoice_data{finance_section} &&
2502 $section->{'description'} eq $invoice_data{finance_section} );
2504 $section->{'subtotal'} = $other_money_char.
2505 sprintf('%.2f', $section->{'subtotal'})
2508 if ( $section->{'description'} ) {
2509 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2515 $options{'section'} = $section if $multisection;
2516 $options{'format'} = $format;
2517 $options{'escape_function'} = $escape_function;
2518 $options{'format_function'} = sub { () } unless $unsquelched;
2519 $options{'unsquelched'} = $unsquelched;
2520 $options{'summary_page'} = $summarypage;
2522 foreach my $line_item ( $self->_items_pkg(%options) ) {
2524 ext_description => [],
2526 $detail->{'ref'} = $line_item->{'pkgnum'};
2527 $detail->{'quantity'} = $line_item->{'quantity'};
2528 $detail->{'section'} = $section;
2529 $detail->{'description'} = &$escape_function($line_item->{'description'});
2530 if ( exists $line_item->{'ext_description'} ) {
2531 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2533 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2534 $line_item->{'amount'};
2535 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2536 $line_item->{'unit_amount'};
2537 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2539 push @detail_items, $detail;
2540 push @buf, ( [ $detail->{'description'},
2541 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2543 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2547 if ( $section->{'description'} ) {
2548 push @buf, ( ['','-----------'],
2549 [ $section->{'description'}. ' sub-total',
2550 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2559 $invoice_data{current_less_finance} =
2560 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2562 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2563 unshift @sections, $previous_section if $pr_total;
2566 foreach my $tax ( $self->_items_tax ) {
2568 $taxtotal += $tax->{'amount'};
2570 my $description = &$escape_function( $tax->{'description'} );
2571 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2573 if ( $multisection ) {
2575 my $money = $old_latex ? '' : $money_char;
2576 push @detail_items, {
2577 ext_description => [],
2580 description => $description,
2581 amount => $money. $amount,
2583 section => $tax_section,
2588 push @total_items, {
2589 'total_item' => $description,
2590 'total_amount' => $other_money_char. $amount,
2595 push @buf,[ $description,
2596 $money_char. $amount,
2603 $total->{'total_item'} = 'Sub-total';
2604 $total->{'total_amount'} =
2605 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2607 if ( $multisection ) {
2608 $tax_section->{'subtotal'} = $other_money_char.
2609 sprintf('%.2f', $taxtotal);
2610 $tax_section->{'pretotal'} = 'New charges sub-total '.
2611 $total->{'total_amount'};
2612 push @sections, $tax_section if $taxtotal;
2614 unshift @total_items, $total;
2617 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2619 push @buf,['','-----------'];
2620 push @buf,[( $conf->exists('disable_previous_balance')
2622 : 'Total New Charges'
2624 $money_char. sprintf("%10.2f",$self->charged) ];
2629 $total->{'total_item'} = &$embolden_function('Total');
2630 $total->{'total_amount'} =
2631 &$embolden_function(
2634 $self->charged + ( $conf->exists('disable_previous_balance')
2640 if ( $multisection ) {
2641 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2642 sprintf('%.2f', $self->charged );
2644 push @total_items, $total;
2646 push @buf,['','-----------'];
2647 push @buf,['Total Charges',
2649 sprintf( '%10.2f', $self->charged +
2650 ( $conf->exists('disable_previous_balance')
2659 unless ( $conf->exists('disable_previous_balance') ) {
2660 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2663 my $credittotal = 0;
2664 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2667 $total->{'total_item'} = &$escape_function($credit->{'description'});
2668 $credittotal += $credit->{'amount'};
2669 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2670 $adjusttotal += $credit->{'amount'};
2671 if ( $multisection ) {
2672 my $money = $old_latex ? '' : $money_char;
2673 push @detail_items, {
2674 ext_description => [],
2677 description => &$escape_function($credit->{'description'}),
2678 amount => $money. $credit->{'amount'},
2680 section => $adjust_section,
2683 push @total_items, $total;
2687 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2690 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2691 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2695 my $paymenttotal = 0;
2696 foreach my $payment ( $self->_items_payments ) {
2698 $total->{'total_item'} = &$escape_function($payment->{'description'});
2699 $paymenttotal += $payment->{'amount'};
2700 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2701 $adjusttotal += $payment->{'amount'};
2702 if ( $multisection ) {
2703 my $money = $old_latex ? '' : $money_char;
2704 push @detail_items, {
2705 ext_description => [],
2708 description => &$escape_function($payment->{'description'}),
2709 amount => $money. $payment->{'amount'},
2711 section => $adjust_section,
2714 push @total_items, $total;
2716 push @buf, [ $payment->{'description'},
2717 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2720 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2722 if ( $multisection ) {
2723 $adjust_section->{'subtotal'} = $other_money_char.
2724 sprintf('%.2f', $adjusttotal);
2725 push @sections, $adjust_section;
2730 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2731 $total->{'total_amount'} =
2732 &$embolden_function(
2733 $other_money_char. sprintf('%.2f', $summarypage
2735 $self->billing_balance
2736 : $self->owed + $pr_total
2739 if ( $multisection ) {
2740 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2741 $total->{'total_amount'};
2743 push @total_items, $total;
2745 push @buf,['','-----------'];
2746 push @buf,[$self->balance_due_msg, $money_char.
2747 sprintf("%10.2f", $balance_due ) ];
2751 if ( $multisection ) {
2752 push @sections, @$late_sections
2756 my @includelist = ();
2757 push @includelist, 'summary' if $summarypage;
2758 foreach my $include ( @includelist ) {
2760 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2763 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2765 @inc_src = $conf->config($inc_file, $agentnum);
2769 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2771 my $convert_map = $convert_maps{$format}{$include};
2773 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2774 s/--\@\]/$delimiters{$format}[1]/g;
2777 &$convert_map( $conf->config($inc_file, $agentnum) );
2781 my $inc_tt = new Text::Template (
2783 SOURCE => [ map "$_\n", @inc_src ],
2784 DELIMITERS => $delimiters{$format},
2785 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2787 unless ( $inc_tt->compile() ) {
2788 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2789 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2793 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2795 $invoice_data{$include} =~ s/\n+$//
2796 if ($format eq 'latex');
2801 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2802 /invoice_lines\((\d*)\)/;
2803 $invoice_lines += $1 || scalar(@buf);
2806 die "no invoice_lines() functions in template?"
2807 if ( $format eq 'template' && !$wasfunc );
2809 if ($format eq 'template') {
2811 if ( $invoice_lines ) {
2812 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2813 $invoice_data{'total_pages'}++
2814 if scalar(@buf) % $invoice_lines;
2817 #setup subroutine for the template
2818 sub FS::cust_bill::_template::invoice_lines {
2819 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2821 scalar(@FS::cust_bill::_template::buf)
2822 ? shift @FS::cust_bill::_template::buf
2831 push @collect, split("\n",
2832 $text_template->fill_in( HASH => \%invoice_data,
2833 PACKAGE => 'FS::cust_bill::_template'
2836 $FS::cust_bill::_template::page++;
2838 map "$_\n", @collect;
2840 warn "filling in template for invoice ". $self->invnum. "\n"
2842 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2845 $text_template->fill_in(HASH => \%invoice_data);
2849 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2851 Returns an postscript invoice, as a scalar.
2853 Options can be passed as a hashref (recommended) or as a list of time, template
2854 and then any key/value pairs for any other options.
2856 I<time> an optional value used to control the printing of overdue messages. The
2857 default is now. It isn't the date of the invoice; that's the `_date' field.
2858 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2859 L<Time::Local> and L<Date::Parse> for conversion functions.
2861 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2868 my ($file, $lfile) = $self->print_latex(@_);
2869 my $ps = generate_ps($file);
2875 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2877 Returns an PDF invoice, as a scalar.
2879 Options can be passed as a hashref (recommended) or as a list of time, template
2880 and then any key/value pairs for any other options.
2882 I<time> an optional value used to control the printing of overdue messages. The
2883 default is now. It isn't the date of the invoice; that's the `_date' field.
2884 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2885 L<Time::Local> and L<Date::Parse> for conversion functions.
2887 I<template>, if specified, is the name of a suffix for alternate invoices.
2889 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2896 my ($file, $lfile) = $self->print_latex(@_);
2897 my $pdf = generate_pdf($file);
2903 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2905 Returns an HTML invoice, as a scalar.
2907 I<time> an optional value used to control the printing of overdue messages. The
2908 default is now. It isn't the date of the invoice; that's the `_date' field.
2909 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2910 L<Time::Local> and L<Date::Parse> for conversion functions.
2912 I<template>, if specified, is the name of a suffix for alternate invoices.
2914 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2916 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2917 when emailing the invoice as part of a multipart/related MIME email.
2925 %params = %{ shift() };
2927 $params{'time'} = shift;
2928 $params{'template'} = shift;
2929 $params{'cid'} = shift;
2932 $params{'format'} = 'html';
2934 $self->print_generic( %params );
2937 # quick subroutine for print_latex
2939 # There are ten characters that LaTeX treats as special characters, which
2940 # means that they do not simply typeset themselves:
2941 # # $ % & ~ _ ^ \ { }
2943 # TeX ignores blanks following an escaped character; if you want a blank (as
2944 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2948 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2949 $value =~ s/([<>])/\$$1\$/g;
2953 #utility methods for print_*
2955 sub _translate_old_latex_format {
2956 warn "_translate_old_latex_format called\n"
2963 if ( $line =~ /^%%Detail\s*$/ ) {
2965 push @template, q![@--!,
2966 q! foreach my $_tr_line (@detail_items) {!,
2967 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2968 q! $_tr_line->{'description'} .= !,
2969 q! "\\tabularnewline\n~~".!,
2970 q! join( "\\tabularnewline\n~~",!,
2971 q! @{$_tr_line->{'ext_description'}}!,
2975 while ( ( my $line_item_line = shift )
2976 !~ /^%%EndDetail\s*$/ ) {
2977 $line_item_line =~ s/'/\\'/g; # nice LTS
2978 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2979 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2980 push @template, " \$OUT .= '$line_item_line';";
2983 push @template, '}',
2986 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2988 push @template, '[@--',
2989 ' foreach my $_tr_line (@total_items) {';
2991 while ( ( my $total_item_line = shift )
2992 !~ /^%%EndTotalDetails\s*$/ ) {
2993 $total_item_line =~ s/'/\\'/g; # nice LTS
2994 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2995 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2996 push @template, " \$OUT .= '$total_item_line';";
2999 push @template, '}',
3003 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3004 push @template, $line;
3010 warn "$_\n" foreach @template;
3019 #check for an invoice- specific override (eventually)
3021 #check for a customer- specific override
3022 return $self->cust_main->invoice_terms
3023 if $self->cust_main->invoice_terms;
3025 #use configured default
3026 $conf->config('invoice_default_terms') || '';
3032 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3033 $duedate = $self->_date() + ( $1 * 86400 );
3040 $self->due_date ? time2str(shift, $self->due_date) : '';
3043 sub balance_due_msg {
3045 my $msg = 'Balance Due';
3046 return $msg unless $self->terms;
3047 if ( $self->due_date ) {
3048 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3049 } elsif ( $self->terms ) {
3050 $msg .= ' - '. $self->terms;
3055 sub balance_due_date {
3058 if ( $conf->exists('invoice_default_terms')
3059 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3060 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3065 =item invnum_date_pretty
3067 Returns a string with the invoice number and date, for example:
3068 "Invoice #54 (3/20/2008)"
3072 sub invnum_date_pretty {
3074 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3079 Returns a string with the date, for example: "3/20/2008"
3085 time2str('%x', $self->_date);
3088 sub _items_sections {
3091 my $summarypage = shift;
3098 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3102 my $usage = $cust_bill_pkg->usage;
3104 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3105 next if ( $display->summary && $summarypage );
3107 my $desc = $display->section;
3108 my $type = $display->type;
3110 if ( $cust_bill_pkg->pkgnum > 0 ) {
3111 $not_tax{$desc} = 1;
3114 if ( $display->post_total && !$summarypage ) {
3115 if (! $type || $type eq 'S') {
3116 $l{$desc} += $cust_bill_pkg->setup
3117 if ( $cust_bill_pkg->setup != 0 );
3121 $l{$desc} += $cust_bill_pkg->recur
3122 if ( $cust_bill_pkg->recur != 0 );
3125 if ($type && $type eq 'R') {
3126 $l{$desc} += $cust_bill_pkg->recur - $usage
3127 if ( $cust_bill_pkg->recur != 0 );
3130 if ($type && $type eq 'U') {
3131 $l{$desc} += $usage;
3135 if (! $type || $type eq 'S') {
3136 $s{$desc} += $cust_bill_pkg->setup
3137 if ( $cust_bill_pkg->setup != 0 );
3141 $s{$desc} += $cust_bill_pkg->recur
3142 if ( $cust_bill_pkg->recur != 0 );
3145 if ($type && $type eq 'R') {
3146 $s{$desc} += $cust_bill_pkg->recur - $usage
3147 if ( $cust_bill_pkg->recur != 0 );
3150 if ($type && $type eq 'U') {
3151 $s{$desc} += $usage;
3160 my %cache = map { $_->categoryname => $_ }
3161 qsearch( 'pkg_category', {disabled => 'Y'} );
3162 $cache{$_->categoryname} = $_
3163 foreach qsearch( 'pkg_category', {disabled => ''} );
3165 push @$late, map { { 'description' => &{$escape}($_),
3166 'subtotal' => $l{$_},
3169 sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
3171 map { { 'description' => &{$escape}($_),
3172 'subtotal' => $s{$_},
3173 'summarized' => $not_tax{$_} ? '' : 'Y',
3174 'tax_section' => $not_tax{$_} ? '' : 'Y',
3176 sort { $cache{$a}->weight <=> $cache{$b}->weight }
3178 ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
3187 #my @display = scalar(@_)
3189 # : qw( _items_previous _items_pkg );
3190 # #: qw( _items_pkg );
3191 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3192 my @display = qw( _items_previous _items_pkg );
3195 foreach my $display ( @display ) {
3196 push @b, $self->$display(@_);
3201 sub _items_previous {
3203 my $cust_main = $self->cust_main;
3204 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3206 foreach ( @pr_cust_bill ) {
3208 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3209 ' ('. time2str('%x',$_->_date). ')',
3210 #'pkgpart' => 'N/A',
3212 'amount' => sprintf("%.2f", $_->owed),
3218 # 'description' => 'Previous Balance',
3219 # #'pkgpart' => 'N/A',
3220 # 'pkgnum' => 'N/A',
3221 # 'amount' => sprintf("%10.2f", $pr_total ),
3222 # 'ext_description' => [ map {
3223 # "Invoice ". $_->invnum.
3224 # " (". time2str("%x",$_->_date). ") ".
3225 # sprintf("%10.2f", $_->owed)
3226 # } @pr_cust_bill ],
3233 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3234 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3238 return 0 unless $a cmp $b;
3239 return -1 if $b eq 'Tax';
3240 return 1 if $a eq 'Tax';
3241 return -1 if $b eq 'Other surcharges';
3242 return 1 if $a eq 'Other surcharges';
3248 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3249 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3252 sub _items_cust_bill_pkg {
3254 my $cust_bill_pkg = shift;
3257 my $format = $opt{format} || '';
3258 my $escape_function = $opt{escape_function} || sub { shift };
3259 my $format_function = $opt{format_function} || '';
3260 my $unsquelched = $opt{unsquelched} || '';
3261 my $section = $opt{section}->{description} if $opt{section};
3262 my $summary_page = $opt{summary_page} || '';
3265 my ($s, $r, $u) = ( undef, undef, undef );
3266 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3269 foreach ( $s, $r, $u ) {
3270 if ( $_ && !$cust_bill_pkg->hidden ) {
3271 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3272 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3278 foreach my $display ( grep { defined($section)
3279 ? $_->section eq $section
3282 grep { $_->summary || !$summary_page }
3283 $cust_bill_pkg->cust_bill_pkg_display
3287 my $type = $display->type;
3289 my $desc = $cust_bill_pkg->desc;
3290 $desc = substr($desc, 0, 50). '...'
3291 if $format eq 'latex' && length($desc) > 50;
3293 my %details_opt = ( 'format' => $format,
3294 'escape_function' => $escape_function,
3295 'format_function' => $format_function,
3298 if ( $cust_bill_pkg->pkgnum > 0 ) {
3300 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3302 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3304 my $description = $desc;
3305 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3308 push @d, map &{$escape_function}($_),
3309 $cust_pkg->h_labels_short($self->_date)
3310 unless $cust_pkg->part_pkg->hide_svc_detail
3311 || $cust_bill_pkg->hidden;
3312 push @d, $cust_bill_pkg->details(%details_opt)
3313 if $cust_bill_pkg->recur == 0;
3315 if ( $cust_bill_pkg->hidden ) {
3316 $s->{amount} += $cust_bill_pkg->setup;
3317 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3318 push @{ $s->{ext_description} }, @d;
3321 description => $description,
3322 #pkgpart => $part_pkg->pkgpart,
3323 pkgnum => $cust_bill_pkg->pkgnum,
3324 amount => $cust_bill_pkg->setup,
3325 unit_amount => $cust_bill_pkg->unitsetup,
3326 quantity => $cust_bill_pkg->quantity,
3327 ext_description => \@d,
3333 if ( $cust_bill_pkg->recur != 0 &&
3334 ( !$type || $type eq 'R' || $type eq 'U' )
3338 my $is_summary = $display->summary;
3339 my $description = ($is_summary && $type && $type eq 'U')
3340 ? "Usage charges" : $desc;
3342 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3343 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3344 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3349 #at least until cust_bill_pkg has "past" ranges in addition to
3350 #the "future" sdate/edate ones... see #3032
3351 my @dates = ( $self->_date );
3352 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3353 push @dates, $prev->sdate if $prev;
3355 push @d, map &{$escape_function}($_),
3356 $cust_pkg->h_labels_short(@dates)
3357 #$cust_bill_pkg->edate,
3358 #$cust_bill_pkg->sdate)
3359 unless $cust_pkg->part_pkg->hide_svc_detail
3360 || $cust_bill_pkg->itemdesc
3361 || $cust_bill_pkg->hidden
3362 || $is_summary && $type && $type eq 'U';
3364 push @d, $cust_bill_pkg->details(%details_opt)
3365 unless ($is_summary || $type && $type eq 'R');
3369 $amount = $cust_bill_pkg->recur;
3370 }elsif($type eq 'R') {
3371 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3372 }elsif($type eq 'U') {
3373 $amount = $cust_bill_pkg->usage;
3376 if ( !$type || $type eq 'R' ) {
3378 if ( $cust_bill_pkg->hidden ) {
3379 $r->{amount} += $amount;
3380 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3381 push @{ $r->{ext_description} }, @d;
3384 description => $description,
3385 #pkgpart => $part_pkg->pkgpart,
3386 pkgnum => $cust_bill_pkg->pkgnum,
3388 unit_amount => $cust_bill_pkg->unitrecur,
3389 quantity => $cust_bill_pkg->quantity,
3390 ext_description => \@d,
3394 } elsif ( $amount ) { # && $type eq 'U'
3396 if ( $cust_bill_pkg->hidden ) {
3397 $u->{amount} += $amount;
3398 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3399 push @{ $u->{ext_description} }, @d;
3402 description => $description,
3403 #pkgpart => $part_pkg->pkgpart,
3404 pkgnum => $cust_bill_pkg->pkgnum,
3406 unit_amount => $cust_bill_pkg->unitrecur,
3407 quantity => $cust_bill_pkg->quantity,
3408 ext_description => \@d,
3414 } # recurring or usage with recurring charge
3416 } else { #pkgnum tax or one-shot line item (??)
3418 if ( $cust_bill_pkg->setup != 0 ) {
3420 'description' => $desc,
3421 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3424 if ( $cust_bill_pkg->recur != 0 ) {
3426 'description' => "$desc (".
3427 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3428 time2str("%x", $cust_bill_pkg->edate). ')',
3429 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3439 foreach ( $s, $r, $u ) {
3441 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3442 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3451 sub _items_credits {
3452 my( $self, %opt ) = @_;
3453 my $trim_len = $opt{'trim_len'} || 60;
3457 foreach ( $self->cust_credited ) {
3459 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3461 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3462 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3463 $reason = " ($reason) " if $reason;
3466 #'description' => 'Credit ref\#'. $_->crednum.
3467 # " (". time2str("%x",$_->cust_credit->_date) .")".
3469 'description' => 'Credit applied '.
3470 time2str("%x",$_->cust_credit->_date). $reason,
3471 'amount' => sprintf("%.2f",$_->amount),
3479 sub _items_payments {
3483 #get & print payments
3484 foreach ( $self->cust_bill_pay ) {
3486 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3489 'description' => "Payment received ".
3490 time2str("%x",$_->cust_pay->_date ),
3491 'amount' => sprintf("%.2f", $_->amount )
3499 =item call_details [ OPTION => VALUE ... ]
3501 Returns an array of CSV strings representing the call details for this invoice
3502 The only option available is the boolean prepend_billed_number
3507 my ($self, %opt) = @_;
3509 my $format_function = sub { shift };
3511 if ($opt{prepend_billed_number}) {
3512 $format_function = sub {
3516 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3521 my @details = map { $_->details( 'format_function' => $format_function,
3522 'escape_function' => sub{ return() },
3526 $self->cust_bill_pkg;
3527 my $header = $details[0];
3528 ( $header, grep { $_ ne $header } @details );
3538 =item process_reprint
3542 sub process_reprint {
3543 process_re_X('print', @_);
3546 =item process_reemail
3550 sub process_reemail {
3551 process_re_X('email', @_);
3559 process_re_X('fax', @_);
3567 process_re_X('ftp', @_);
3574 sub process_respool {
3575 process_re_X('spool', @_);
3578 use Storable qw(thaw);
3582 my( $method, $job ) = ( shift, shift );
3583 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3585 my $param = thaw(decode_base64(shift));
3586 warn Dumper($param) if $DEBUG;
3597 my($method, $job, %param ) = @_;
3599 warn "re_X $method for job $job with param:\n".
3600 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3603 #some false laziness w/search/cust_bill.html
3605 my $orderby = 'ORDER BY cust_bill._date';
3607 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3609 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3611 my @cust_bill = qsearch( {
3612 #'select' => "cust_bill.*",
3613 'table' => 'cust_bill',
3614 'addl_from' => $addl_from,
3616 'extra_sql' => $extra_sql,
3617 'order_by' => $orderby,
3621 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3623 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3626 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3627 foreach my $cust_bill ( @cust_bill ) {
3628 $cust_bill->$method();
3630 if ( $job ) { #progressbar foo
3632 if ( time - $min_sec > $last ) {
3633 my $error = $job->update_statustext(
3634 int( 100 * $num / scalar(@cust_bill) )
3636 die $error if $error;
3647 =head1 CLASS METHODS
3653 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3659 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3664 Returns an SQL fragment to retreive the net amount (charged minus credited).
3670 'charged - '. $class->credited_sql;
3675 Returns an SQL fragment to retreive the amount paid against this invoice.
3681 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3682 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3687 Returns an SQL fragment to retreive the amount credited against this invoice.
3693 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3694 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3697 =item search_sql HASHREF
3699 Class method which returns an SQL WHERE fragment to search for parameters
3700 specified in HASHREF. Valid parameters are
3706 Epoch date (UNIX timestamp) setting a lower bound for _date values
3710 Epoch date (UNIX timestamp) setting an upper bound for _date values
3724 =item newest_percust
3728 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3733 my($class, $param) = @_;
3735 warn "$me search_sql called with params: \n".
3736 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3741 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3742 push @search, "cust_bill._date >= $1";
3744 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3745 push @search, "cust_bill._date < $1";
3747 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3748 push @search, "cust_bill.invnum >= $1";
3750 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3751 push @search, "cust_bill.invnum <= $1";
3753 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3754 push @search, "cust_main.agentnum = $1";
3757 push @search, '0 != '. FS::cust_bill->owed_sql
3758 if $param->{'open'};
3760 push @search, '0 != '. FS::cust_bill->net_sql
3763 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3764 if $param->{'days'};
3766 if ( $param->{'newest_percust'} ) {
3768 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3769 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3771 my @newest_where = map { my $x = $_;
3772 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3775 grep ! /^cust_main./, @search;
3776 my $newest_where = scalar(@newest_where)
3777 ? ' AND '. join(' AND ', @newest_where)
3781 push @search, "cust_bill._date = (
3782 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3783 WHERE newest_cust_bill.custnum = cust_bill.custnum
3789 my $curuser = $FS::CurrentUser::CurrentUser;
3790 if ( $curuser->username eq 'fs_queue'
3791 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3793 my $newuser = qsearchs('access_user', {
3794 'username' => $username,
3798 $curuser = $newuser;
3800 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3804 push @search, $curuser->agentnums_sql;
3806 join(' AND ', @search );
3818 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3819 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base