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
99 =item invoice_terms - optional terms override for this specific invoice
103 Customer info at invoice generation time
107 =item previous_balance
109 =item billing_balance
117 =item printed - deprecated
125 =item closed - books closed flag, empty or `Y'
127 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
129 =item agent_invid - legacy invoice number
139 Creates a new invoice. To add the invoice to the database, see L<"insert">.
140 Invoices are normally created by calling the bill method of a customer object
141 (see L<FS::cust_main>).
145 sub table { 'cust_bill'; }
147 sub cust_linked { $_[0]->cust_main_custnum; }
148 sub cust_unlinked_msg {
150 "WARNING: can't find cust_main.custnum ". $self->custnum.
151 ' (cust_bill.invnum '. $self->invnum. ')';
156 Adds this invoice to the database ("Posts" the invoice). If there is an error,
157 returns the error, otherwise returns false.
161 This method now works but you probably shouldn't use it. Instead, apply a
162 credit against the invoice.
164 Using this method to delete invoices outright is really, really bad. There
165 would be no record you ever posted this invoice, and there are no check to
166 make sure charged = 0 or that there are no associated cust_bill_pkg records.
168 Really, don't use it.
174 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
176 local $SIG{HUP} = 'IGNORE';
177 local $SIG{INT} = 'IGNORE';
178 local $SIG{QUIT} = 'IGNORE';
179 local $SIG{TERM} = 'IGNORE';
180 local $SIG{TSTP} = 'IGNORE';
181 local $SIG{PIPE} = 'IGNORE';
183 my $oldAutoCommit = $FS::UID::AutoCommit;
184 local $FS::UID::AutoCommit = 0;
187 foreach my $table (qw(
199 foreach my $linked ( $self->$table() ) {
200 my $error = $linked->delete;
202 $dbh->rollback if $oldAutoCommit;
209 my $error = $self->SUPER::delete(@_);
211 $dbh->rollback if $oldAutoCommit;
215 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
221 =item replace OLD_RECORD
223 Replaces the OLD_RECORD with this one in the database. If there is an error,
224 returns the error, otherwise returns false.
226 Only printed may be changed. printed is normally updated by calling the
227 collect method of a customer object (see L<FS::cust_main>).
231 #replace can be inherited from Record.pm
233 # replace_check is now the preferred way to #implement replace data checks
234 # (so $object->replace() works without an argument)
237 my( $new, $old ) = ( shift, shift );
238 return "Can't change custnum!" unless $old->custnum == $new->custnum;
239 #return "Can't change _date!" unless $old->_date eq $new->_date;
240 return "Can't change _date!" unless $old->_date == $new->_date;
241 return "Can't change charged!" unless $old->charged == $new->charged
242 || $old->charged == 0;
249 Checks all fields to make sure this is a valid invoice. If there is an error,
250 returns the error, otherwise returns false. Called by the insert and replace
259 $self->ut_numbern('invnum')
260 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
261 || $self->ut_numbern('_date')
262 || $self->ut_money('charged')
263 || $self->ut_numbern('printed')
264 || $self->ut_enum('closed', [ '', 'Y' ])
265 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
266 || $self->ut_numbern('agent_invid') #varchar?
268 return $error if $error;
270 $self->_date(time) unless $self->_date;
272 $self->printed(0) if $self->printed eq '';
279 Returns the displayed invoice number for this invoice: agent_invid if
280 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
286 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
287 return $self->agent_invid;
289 return $self->invnum;
295 Returns a list consisting of the total previous balance for this customer,
296 followed by the previous outstanding invoices (as FS::cust_bill objects also).
303 my @cust_bill = sort { $a->_date <=> $b->_date }
304 grep { $_->owed != 0 && $_->_date < $self->_date }
305 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
307 foreach ( @cust_bill ) { $total += $_->owed; }
313 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
320 { 'table' => 'cust_bill_pkg',
321 'hashref' => { 'invnum' => $self->invnum },
322 'order_by' => 'ORDER BY billpkgnum',
327 =item cust_bill_pkg_pkgnum PKGNUM
329 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
334 sub cust_bill_pkg_pkgnum {
335 my( $self, $pkgnum ) = @_;
337 { 'table' => 'cust_bill_pkg',
338 'hashref' => { 'invnum' => $self->invnum,
341 'order_by' => 'ORDER BY billpkgnum',
348 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
355 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
357 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
360 =item open_cust_bill_pkg
362 Returns the open line items for this invoice.
364 Note that cust_bill_pkg with both setup and recur fees are returned as two
365 separate line items, each with only one fee.
369 # modeled after cust_main::open_cust_bill
370 sub open_cust_bill_pkg {
373 # grep { $_->owed > 0 } $self->cust_bill_pkg
375 my %other = ( 'recur' => 'setup',
376 'setup' => 'recur', );
378 foreach my $field ( qw( recur setup )) {
379 push @open, map { $_->set( $other{$field}, 0 ); $_; }
380 grep { $_->owed($field) > 0 }
381 $self->cust_bill_pkg;
387 =item cust_bill_event
389 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
393 sub cust_bill_event {
395 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
398 =item num_cust_bill_event
400 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
404 sub num_cust_bill_event {
407 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
408 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
409 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
410 $sth->fetchrow_arrayref->[0];
415 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
419 #false laziness w/cust_pkg.pm
423 'table' => 'cust_event',
424 'addl_from' => 'JOIN part_event USING ( eventpart )',
425 'hashref' => { 'tablenum' => $self->invnum },
426 'extra_sql' => " AND eventtable = 'cust_bill' ",
432 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
436 #false laziness w/cust_pkg.pm
440 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
441 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
442 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
443 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
444 $sth->fetchrow_arrayref->[0];
449 Returns the customer (see L<FS::cust_main>) for this invoice.
455 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
458 =item cust_suspend_if_balance_over AMOUNT
460 Suspends the customer associated with this invoice if the total amount owed on
461 this invoice and all older invoices is greater than the specified amount.
463 Returns a list: an empty list on success or a list of errors.
467 sub cust_suspend_if_balance_over {
468 my( $self, $amount ) = ( shift, shift );
469 my $cust_main = $self->cust_main;
470 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
473 $cust_main->suspend(@_);
479 Depreciated. See the cust_credited method.
481 #Returns a list consisting of the total previous credited (see
482 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
483 #outstanding credits (FS::cust_credit objects).
489 croak "FS::cust_bill->cust_credit depreciated; see ".
490 "FS::cust_bill->cust_credit_bill";
493 #my @cust_credit = sort { $a->_date <=> $b->_date }
494 # grep { $_->credited != 0 && $_->_date < $self->_date }
495 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
497 #foreach (@cust_credit) { $total += $_->credited; }
498 #$total, @cust_credit;
503 Depreciated. See the cust_bill_pay method.
505 #Returns all payments (see L<FS::cust_pay>) for this invoice.
511 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
513 #sort { $a->_date <=> $b->_date }
514 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
520 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
523 sub cust_bill_pay_batch {
525 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
530 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
536 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
537 sort { $a->_date <=> $b->_date }
538 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
543 =item cust_credit_bill
545 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
551 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
552 sort { $a->_date <=> $b->_date }
553 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
557 sub cust_credit_bill {
558 shift->cust_credited(@_);
561 =item cust_bill_pay_pkgnum PKGNUM
563 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
564 with matching pkgnum.
568 sub cust_bill_pay_pkgnum {
569 my( $self, $pkgnum ) = @_;
570 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
571 sort { $a->_date <=> $b->_date }
572 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
578 =item cust_credited_pkgnum PKGNUM
580 =item cust_credit_bill_pkgnum PKGNUM
582 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
583 with matching pkgnum.
587 sub cust_credited_pkgnum {
588 my( $self, $pkgnum ) = @_;
589 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
590 sort { $a->_date <=> $b->_date }
591 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
597 sub cust_credit_bill_pkgnum {
598 shift->cust_credited_pkgnum(@_);
603 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
610 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
612 foreach (@taxlines) { $total += $_->setup; }
618 Returns the amount owed (still outstanding) on this invoice, which is charged
619 minus all payment applications (see L<FS::cust_bill_pay>) and credit
620 applications (see L<FS::cust_credit_bill>).
626 my $balance = $self->charged;
627 $balance -= $_->amount foreach ( $self->cust_bill_pay );
628 $balance -= $_->amount foreach ( $self->cust_credited );
629 $balance = sprintf( "%.2f", $balance);
630 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
635 my( $self, $pkgnum ) = @_;
637 #my $balance = $self->charged;
639 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
641 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
642 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
644 $balance = sprintf( "%.2f", $balance);
645 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
649 =item apply_payments_and_credits [ OPTION => VALUE ... ]
651 Applies unapplied payments and credits to this invoice.
653 A hash of optional arguments may be passed. Currently "manual" is supported.
654 If true, a payment receipt is sent instead of a statement when
655 'payment_receipt_email' configuration option is set.
657 If there is an error, returns the error, otherwise returns false.
661 sub apply_payments_and_credits {
662 my( $self, %options ) = @_;
664 local $SIG{HUP} = 'IGNORE';
665 local $SIG{INT} = 'IGNORE';
666 local $SIG{QUIT} = 'IGNORE';
667 local $SIG{TERM} = 'IGNORE';
668 local $SIG{TSTP} = 'IGNORE';
669 local $SIG{PIPE} = 'IGNORE';
671 my $oldAutoCommit = $FS::UID::AutoCommit;
672 local $FS::UID::AutoCommit = 0;
675 $self->select_for_update; #mutex
677 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
678 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
680 if ( $conf->exists('pkg-balances') ) {
681 # limit @payments & @credits to those w/ a pkgnum grepped from $self
682 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
683 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
684 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
687 while ( $self->owed > 0 and ( @payments || @credits ) ) {
690 if ( @payments && @credits ) {
692 #decide which goes first by weight of top (unapplied) line item
694 my @open_lineitems = $self->open_cust_bill_pkg;
697 max( map { $_->part_pkg->pay_weight || 0 }
702 my $max_credit_weight =
703 max( map { $_->part_pkg->credit_weight || 0 }
709 #if both are the same... payments first? it has to be something
710 if ( $max_pay_weight >= $max_credit_weight ) {
716 } elsif ( @payments ) {
718 } elsif ( @credits ) {
721 die "guru meditation #12 and 35";
725 if ( $app eq 'pay' ) {
727 my $payment = shift @payments;
728 $unapp_amount = $payment->unapplied;
729 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
730 $app->pkgnum( $payment->pkgnum )
731 if $conf->exists('pkg-balances') && $payment->pkgnum;
733 } elsif ( $app eq 'credit' ) {
735 my $credit = shift @credits;
736 $unapp_amount = $credit->credited;
737 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
738 $app->pkgnum( $credit->pkgnum )
739 if $conf->exists('pkg-balances') && $credit->pkgnum;
742 die "guru meditation #12 and 35";
746 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
747 warn "owed_pkgnum ". $app->pkgnum;
748 $owed = $self->owed_pkgnum($app->pkgnum);
752 next unless $owed > 0;
754 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
755 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
757 $app->invnum( $self->invnum );
759 my $error = $app->insert(%options);
761 $dbh->rollback if $oldAutoCommit;
762 return "Error inserting ". $app->table. " record: $error";
764 die $error if $error;
768 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
773 =item generate_email OPTION => VALUE ...
781 sender address, required
785 alternate template name, optional
789 text attachment arrayref, optional
793 email subject, optional
797 notice name instead of "Invoice", optional
801 Returns an argument list to be passed to L<FS::Misc::send_email>.
812 my $me = '[FS::cust_bill::generate_email]';
815 'from' => $args{'from'},
816 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
820 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
821 'template' => $args{'template'},
822 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
825 my $cust_main = $self->cust_main;
827 if (ref($args{'to'}) eq 'ARRAY') {
828 $return{'to'} = $args{'to'};
830 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
831 $cust_main->invoicing_list
835 if ( $conf->exists('invoice_html') ) {
837 warn "$me creating HTML/text multipart message"
840 $return{'nobody'} = 1;
842 my $alternative = build MIME::Entity
843 'Type' => 'multipart/alternative',
844 'Encoding' => '7bit',
845 'Disposition' => 'inline'
849 if ( $conf->exists('invoice_email_pdf')
850 and scalar($conf->config('invoice_email_pdf_note')) ) {
852 warn "$me using 'invoice_email_pdf_note' in multipart message"
854 $data = [ map { $_ . "\n" }
855 $conf->config('invoice_email_pdf_note')
860 warn "$me not using 'invoice_email_pdf_note' in multipart message"
862 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
863 $data = $args{'print_text'};
865 $data = [ $self->print_text(\%opt) ];
870 $alternative->attach(
871 'Type' => 'text/plain',
872 #'Encoding' => 'quoted-printable',
873 'Encoding' => '7bit',
875 'Disposition' => 'inline',
878 $args{'from'} =~ /\@([\w\.\-]+)/;
879 my $from = $1 || 'example.com';
880 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
883 my $agentnum = $cust_main->agentnum;
884 if ( defined($args{'template'}) && length($args{'template'})
885 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
888 $logo = 'logo_'. $args{'template'}. '.png';
892 my $image_data = $conf->config_binary( $logo, $agentnum);
894 my $image = build MIME::Entity
895 'Type' => 'image/png',
896 'Encoding' => 'base64',
897 'Data' => $image_data,
898 'Filename' => 'logo.png',
899 'Content-ID' => "<$content_id>",
902 $alternative->attach(
903 'Type' => 'text/html',
904 'Encoding' => 'quoted-printable',
905 'Data' => [ '<html>',
908 ' '. encode_entities($return{'subject'}),
911 ' <body bgcolor="#e8e8e8">',
912 $self->print_html({ 'cid'=>$content_id, %opt }),
916 'Disposition' => 'inline',
917 #'Filename' => 'invoice.pdf',
921 if ( $cust_main->email_csv_cdr ) {
923 push @otherparts, build MIME::Entity
924 'Type' => 'text/csv',
925 'Encoding' => '7bit',
926 'Data' => [ map { "$_\n" }
927 $self->call_details('prepend_billed_number' => 1)
929 'Disposition' => 'attachment',
930 'Filename' => 'usage-'. $self->invnum. '.csv',
935 if ( $conf->exists('invoice_email_pdf') ) {
940 # multipart/alternative
946 my $related = build MIME::Entity 'Type' => 'multipart/related',
947 'Encoding' => '7bit';
949 #false laziness w/Misc::send_email
950 $related->head->replace('Content-type',
952 '; boundary="'. $related->head->multipart_boundary. '"'.
953 '; type=multipart/alternative'
956 $related->add_part($alternative);
958 $related->add_part($image);
960 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
962 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
966 #no other attachment:
968 # multipart/alternative
973 $return{'content-type'} = 'multipart/related';
974 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
975 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
976 #$return{'disposition'} = 'inline';
982 if ( $conf->exists('invoice_email_pdf') ) {
983 warn "$me creating PDF attachment"
986 #mime parts arguments a la MIME::Entity->build().
987 $return{'mimeparts'} = [
988 { $self->mimebuild_pdf(\%opt) }
992 if ( $conf->exists('invoice_email_pdf')
993 and scalar($conf->config('invoice_email_pdf_note')) ) {
995 warn "$me using 'invoice_email_pdf_note'"
997 $return{'body'} = [ map { $_ . "\n" }
998 $conf->config('invoice_email_pdf_note')
1003 warn "$me not using 'invoice_email_pdf_note'"
1005 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1006 $return{'body'} = $args{'print_text'};
1008 $return{'body'} = [ $self->print_text(\%opt) ];
1021 Returns a list suitable for passing to MIME::Entity->build(), representing
1022 this invoice as PDF attachment.
1029 'Type' => 'application/pdf',
1030 'Encoding' => 'base64',
1031 'Data' => [ $self->print_pdf(@_) ],
1032 'Disposition' => 'attachment',
1033 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1037 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1039 Sends this invoice to the destinations configured for this customer: sends
1040 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1042 Options can be passed as a hashref (recommended) or as a list of up to
1043 four values for templatename, agentnum, invoice_from and amount.
1045 I<template>, if specified, is the name of a suffix for alternate invoices.
1047 I<agentnum>, if specified, means that this invoice will only be sent for customers
1048 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1049 single agent) or an arrayref of agentnums.
1051 I<invoice_from>, if specified, overrides the default email invoice From: address.
1053 I<amount>, if specified, only sends the invoice if the total amount owed on this
1054 invoice and all older invoices is greater than the specified amount.
1056 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1060 sub queueable_send {
1063 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1064 or die "invalid invoice number: " . $opt{invnum};
1066 my @args = ( $opt{template}, $opt{agentnum} );
1067 push @args, $opt{invoice_from}
1068 if exists($opt{invoice_from}) && $opt{invoice_from};
1070 my $error = $self->send( @args );
1071 die $error if $error;
1078 my( $template, $invoice_from, $notice_name );
1080 my $balance_over = 0;
1084 $template = $opt->{'template'} || '';
1085 if ( $agentnums = $opt->{'agentnum'} ) {
1086 $agentnums = [ $agentnums ] unless ref($agentnums);
1088 $invoice_from = $opt->{'invoice_from'};
1089 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1090 $notice_name = $opt->{'notice_name'};
1092 $template = scalar(@_) ? shift : '';
1093 if ( scalar(@_) && $_[0] ) {
1094 $agentnums = ref($_[0]) ? shift : [ shift ];
1096 $invoice_from = shift if scalar(@_);
1097 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1100 return 'N/A' unless ! $agentnums
1101 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1104 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1106 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1107 $conf->config('invoice_from', $self->cust_main->agentnum );
1110 'template' => $template,
1111 'invoice_from' => $invoice_from,
1112 'notice_name' => ( $notice_name || 'Invoice' ),
1115 my @invoicing_list = $self->cust_main->invoicing_list;
1117 #$self->email_invoice(\%opt)
1119 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1121 #$self->print_invoice(\%opt)
1123 if grep { $_ eq 'POST' } @invoicing_list; #postal
1125 $self->fax_invoice(\%opt)
1126 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1132 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1134 Emails this invoice.
1136 Options can be passed as a hashref (recommended) or as a list of up to
1137 two values for templatename and invoice_from.
1139 I<template>, if specified, is the name of a suffix for alternate invoices.
1141 I<invoice_from>, if specified, overrides the default email invoice From: address.
1143 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1147 sub queueable_email {
1150 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1151 or die "invalid invoice number: " . $opt{invnum};
1153 my @args = ( $opt{template} );
1154 push @args, $opt{invoice_from}
1155 if exists($opt{invoice_from}) && $opt{invoice_from};
1157 my $error = $self->email( @args );
1158 die $error if $error;
1162 #sub email_invoice {
1166 my( $template, $invoice_from, $notice_name );
1169 $template = $opt->{'template'} || '';
1170 $invoice_from = $opt->{'invoice_from'};
1171 $notice_name = $opt->{'notice_name'} || 'Invoice';
1173 $template = scalar(@_) ? shift : '';
1174 $invoice_from = shift if scalar(@_);
1175 $notice_name = 'Invoice';
1178 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1179 $conf->config('invoice_from', $self->cust_main->agentnum );
1181 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1182 $self->cust_main->invoicing_list;
1184 #better to notify this person than silence
1185 @invoicing_list = ($invoice_from) unless @invoicing_list;
1187 my $subject = $self->email_subject($template);
1189 my $error = send_email(
1190 $self->generate_email(
1191 'from' => $invoice_from,
1192 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1193 'subject' => $subject,
1194 'template' => $template,
1195 'notice_name' => $notice_name,
1198 die "can't email invoice: $error\n" if $error;
1199 #die "$error\n" if $error;
1206 #my $template = scalar(@_) ? shift : '';
1209 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1212 my $cust_main = $self->cust_main;
1213 my $name = $cust_main->name;
1214 my $name_short = $cust_main->name_short;
1215 my $invoice_number = $self->invnum;
1216 my $invoice_date = $self->_date_pretty;
1218 eval qq("$subject");
1221 =item lpr_data HASHREF | [ TEMPLATE ]
1223 Returns the postscript or plaintext for this invoice as an arrayref.
1225 Options can be passed as a hashref (recommended) or as a single optional value
1228 I<template>, if specified, is the name of a suffix for alternate invoices.
1230 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1236 my( $template, $notice_name );
1239 $template = $opt->{'template'} || '';
1240 $notice_name = $opt->{'notice_name'} || 'Invoice';
1242 $template = scalar(@_) ? shift : '';
1243 $notice_name = 'Invoice';
1247 'template' => $template,
1248 'notice_name' => $notice_name,
1251 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1252 [ $self->$method( \%opt ) ];
1255 =item print HASHREF | [ TEMPLATE ]
1257 Prints this invoice.
1259 Options can be passed as a hashref (recommended) or as a single optional
1262 I<template>, if specified, is the name of a suffix for alternate invoices.
1264 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1268 #sub print_invoice {
1271 my( $template, $notice_name );
1274 $template = $opt->{'template'} || '';
1275 $notice_name = $opt->{'notice_name'} || 'Invoice';
1277 $template = scalar(@_) ? shift : '';
1278 $notice_name = 'Invoice';
1282 'template' => $template,
1283 'notice_name' => $notice_name,
1286 do_print $self->lpr_data(\%opt);
1289 =item fax_invoice HASHREF | [ TEMPLATE ]
1293 Options can be passed as a hashref (recommended) or as a single optional
1296 I<template>, if specified, is the name of a suffix for alternate invoices.
1298 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1304 my( $template, $notice_name );
1307 $template = $opt->{'template'} || '';
1308 $notice_name = $opt->{'notice_name'} || 'Invoice';
1310 $template = scalar(@_) ? shift : '';
1311 $notice_name = 'Invoice';
1314 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1315 unless $conf->exists('invoice_latex');
1317 my $dialstring = $self->cust_main->getfield('fax');
1321 'template' => $template,
1322 'notice_name' => $notice_name,
1325 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1326 'dialstring' => $dialstring,
1328 die $error if $error;
1332 =item ftp_invoice [ TEMPLATENAME ]
1334 Sends this invoice data via FTP.
1336 TEMPLATENAME is unused?
1342 my $template = scalar(@_) ? shift : '';
1345 'protocol' => 'ftp',
1346 'server' => $conf->config('cust_bill-ftpserver'),
1347 'username' => $conf->config('cust_bill-ftpusername'),
1348 'password' => $conf->config('cust_bill-ftppassword'),
1349 'dir' => $conf->config('cust_bill-ftpdir'),
1350 'format' => $conf->config('cust_bill-ftpformat'),
1354 =item spool_invoice [ TEMPLATENAME ]
1356 Spools this invoice data (see L<FS::spool_csv>)
1358 TEMPLATENAME is unused?
1364 my $template = scalar(@_) ? shift : '';
1367 'format' => $conf->config('cust_bill-spoolformat'),
1368 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1372 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1374 Like B<send>, but only sends the invoice if it is the newest open invoice for
1379 sub send_if_newest {
1384 grep { $_->owed > 0 }
1385 qsearch('cust_bill', {
1386 'custnum' => $self->custnum,
1387 #'_date' => { op=>'>', value=>$self->_date },
1388 'invnum' => { op=>'>', value=>$self->invnum },
1395 =item send_csv OPTION => VALUE, ...
1397 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1401 protocol - currently only "ftp"
1407 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1408 and YYMMDDHHMMSS is a timestamp.
1410 See L</print_csv> for a description of the output format.
1415 my($self, %opt) = @_;
1419 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1420 mkdir $spooldir, 0700 unless -d $spooldir;
1422 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1423 my $file = "$spooldir/$tracctnum.csv";
1425 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1427 open(CSV, ">$file") or die "can't open $file: $!";
1435 if ( $opt{protocol} eq 'ftp' ) {
1436 eval "use Net::FTP;";
1438 $net = Net::FTP->new($opt{server}) or die @$;
1440 die "unknown protocol: $opt{protocol}";
1443 $net->login( $opt{username}, $opt{password} )
1444 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1446 $net->binary or die "can't set binary mode";
1448 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1450 $net->put($file) or die "can't put $file: $!";
1460 Spools CSV invoice data.
1466 =item format - 'default' or 'billco'
1468 =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>).
1470 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1472 =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.
1479 my($self, %opt) = @_;
1481 my $cust_main = $self->cust_main;
1483 if ( $opt{'dest'} ) {
1484 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1485 $cust_main->invoicing_list;
1486 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1487 || ! keys %invoicing_list;
1490 if ( $opt{'balanceover'} ) {
1492 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1495 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1496 mkdir $spooldir, 0700 unless -d $spooldir;
1498 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1502 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1503 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1506 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1508 open(CSV, ">>$file") or die "can't open $file: $!";
1509 flock(CSV, LOCK_EX);
1514 if ( lc($opt{'format'}) eq 'billco' ) {
1516 flock(CSV, LOCK_UN);
1521 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1524 open(CSV,">>$file") or die "can't open $file: $!";
1525 flock(CSV, LOCK_EX);
1531 flock(CSV, LOCK_UN);
1538 =item print_csv OPTION => VALUE, ...
1540 Returns CSV data for this invoice.
1544 format - 'default' or 'billco'
1546 Returns a list consisting of two scalars. The first is a single line of CSV
1547 header information for this invoice. The second is one or more lines of CSV
1548 detail information for this invoice.
1550 If I<format> is not specified or "default", the fields of the CSV file are as
1553 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1557 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1559 B<record_type> is C<cust_bill> for the initial header line only. The
1560 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1561 fields are filled in.
1563 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1564 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1567 =item invnum - invoice number
1569 =item custnum - customer number
1571 =item _date - invoice date
1573 =item charged - total invoice amount
1575 =item first - customer first name
1577 =item last - customer first name
1579 =item company - company name
1581 =item address1 - address line 1
1583 =item address2 - address line 1
1593 =item pkg - line item description
1595 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1597 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1599 =item sdate - start date for recurring fee
1601 =item edate - end date for recurring fee
1605 If I<format> is "billco", the fields of the header CSV file are as follows:
1607 +-------------------------------------------------------------------+
1608 | FORMAT HEADER FILE |
1609 |-------------------------------------------------------------------|
1610 | Field | Description | Name | Type | Width |
1611 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1612 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1613 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1614 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1615 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1616 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1617 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1618 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1619 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1620 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1621 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1622 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1623 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1624 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1625 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1626 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1627 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1628 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1629 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1630 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1631 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1632 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1633 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1634 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1635 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1636 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1637 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1638 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1639 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1640 +-------+-------------------------------+------------+------+-------+
1642 If I<format> is "billco", the fields of the detail CSV file are as follows:
1644 FORMAT FOR DETAIL FILE
1646 Field | Description | Name | Type | Width
1647 1 | N/A-Leave Empty | RC | CHAR | 2
1648 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1649 3 | Account Number | TRACCTNUM | CHAR | 15
1650 4 | Invoice Number | TRINVOICE | CHAR | 15
1651 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1652 6 | Transaction Detail | DETAILS | CHAR | 100
1653 7 | Amount | AMT | NUM* | 9
1654 8 | Line Format Control** | LNCTRL | CHAR | 2
1655 9 | Grouping Code | GROUP | CHAR | 2
1656 10 | User Defined | ACCT CODE | CHAR | 15
1661 my($self, %opt) = @_;
1663 eval "use Text::CSV_XS";
1666 my $cust_main = $self->cust_main;
1668 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1670 if ( lc($opt{'format'}) eq 'billco' ) {
1673 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1675 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1677 my( $previous_balance, @unused ) = $self->previous; #previous balance
1679 my $pmt_cr_applied = 0;
1680 $pmt_cr_applied += $_->{'amount'}
1681 foreach ( $self->_items_payments, $self->_items_credits ) ;
1683 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1686 '', # 1 | N/A-Leave Empty CHAR 2
1687 '', # 2 | N/A-Leave Empty CHAR 15
1688 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1689 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1690 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1691 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1692 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1693 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1694 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1695 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1696 '', # 10 | Ancillary Billing Information CHAR 30
1697 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1698 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1701 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1704 $duedate, # 14 | Bill Due Date CHAR 10
1706 $previous_balance, # 15 | Previous Balance NUM* 9
1707 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1708 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1709 $totaldue, # 18 | Total Amt Due NUM* 9
1710 $totaldue, # 19 | Total Amt Due NUM* 9
1711 '', # 20 | 30 Day Aging NUM* 9
1712 '', # 21 | 60 Day Aging NUM* 9
1713 '', # 22 | 90 Day Aging NUM* 9
1714 'N', # 23 | Y/N CHAR 1
1715 '', # 24 | Remittance automation CHAR 100
1716 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1717 $self->custnum, # 26 | Customer Reference Number CHAR 15
1718 '0', # 27 | Federal Tax*** NUM* 9
1719 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1720 '0', # 29 | Other Taxes & Fees*** NUM* 9
1729 time2str("%x", $self->_date),
1730 sprintf("%.2f", $self->charged),
1731 ( map { $cust_main->getfield($_) }
1732 qw( first last company address1 address2 city state zip country ) ),
1734 ) or die "can't create csv";
1737 my $header = $csv->string. "\n";
1740 if ( lc($opt{'format'}) eq 'billco' ) {
1743 foreach my $item ( $self->_items_pkg ) {
1746 '', # 1 | N/A-Leave Empty CHAR 2
1747 '', # 2 | N/A-Leave Empty CHAR 15
1748 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1749 $self->invnum, # 4 | Invoice Number CHAR 15
1750 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1751 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1752 $item->{'amount'}, # 7 | Amount NUM* 9
1753 '', # 8 | Line Format Control** CHAR 2
1754 '', # 9 | Grouping Code CHAR 2
1755 '', # 10 | User Defined CHAR 15
1758 $detail .= $csv->string. "\n";
1764 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1766 my($pkg, $setup, $recur, $sdate, $edate);
1767 if ( $cust_bill_pkg->pkgnum ) {
1769 ($pkg, $setup, $recur, $sdate, $edate) = (
1770 $cust_bill_pkg->part_pkg->pkg,
1771 ( $cust_bill_pkg->setup != 0
1772 ? sprintf("%.2f", $cust_bill_pkg->setup )
1774 ( $cust_bill_pkg->recur != 0
1775 ? sprintf("%.2f", $cust_bill_pkg->recur )
1777 ( $cust_bill_pkg->sdate
1778 ? time2str("%x", $cust_bill_pkg->sdate)
1780 ($cust_bill_pkg->edate
1781 ?time2str("%x", $cust_bill_pkg->edate)
1785 } else { #pkgnum tax
1786 next unless $cust_bill_pkg->setup != 0;
1787 $pkg = $cust_bill_pkg->desc;
1788 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1789 ( $sdate, $edate ) = ( '', '' );
1795 ( map { '' } (1..11) ),
1796 ($pkg, $setup, $recur, $sdate, $edate)
1797 ) or die "can't create csv";
1799 $detail .= $csv->string. "\n";
1805 ( $header, $detail );
1811 Pays this invoice with a compliemntary payment. If there is an error,
1812 returns the error, otherwise returns false.
1818 my $cust_pay = new FS::cust_pay ( {
1819 'invnum' => $self->invnum,
1820 'paid' => $self->owed,
1823 'payinfo' => $self->cust_main->payinfo,
1831 Attempts to pay this invoice with a credit card payment via a
1832 Business::OnlinePayment realtime gateway. See
1833 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1834 for supported processors.
1840 $self->realtime_bop( 'CC', @_ );
1845 Attempts to pay this invoice with an electronic check (ACH) payment via a
1846 Business::OnlinePayment realtime gateway. See
1847 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1848 for supported processors.
1854 $self->realtime_bop( 'ECHECK', @_ );
1859 Attempts to pay this invoice with phone bill (LEC) payment via a
1860 Business::OnlinePayment realtime gateway. See
1861 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1862 for supported processors.
1868 $self->realtime_bop( 'LEC', @_ );
1872 my( $self, $method ) = @_;
1874 my $cust_main = $self->cust_main;
1875 my $balance = $cust_main->balance;
1876 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1877 $amount = sprintf("%.2f", $amount);
1878 return "not run (balance $balance)" unless $amount > 0;
1880 my $description = 'Internet Services';
1881 if ( $conf->exists('business-onlinepayment-description') ) {
1882 my $dtempl = $conf->config('business-onlinepayment-description');
1884 my $agent_obj = $cust_main->agent
1885 or die "can't retreive agent for $cust_main (agentnum ".
1886 $cust_main->agentnum. ")";
1887 my $agent = $agent_obj->agent;
1888 my $pkgs = join(', ',
1889 map { $_->part_pkg->pkg }
1890 grep { $_->pkgnum } $self->cust_bill_pkg
1892 $description = eval qq("$dtempl");
1895 $cust_main->realtime_bop($method, $amount,
1896 'description' => $description,
1897 'invnum' => $self->invnum,
1902 =item batch_card OPTION => VALUE...
1904 Adds a payment for this invoice to the pending credit card batch (see
1905 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1906 runs the payment using a realtime gateway.
1911 my ($self, %options) = @_;
1912 my $cust_main = $self->cust_main;
1914 $options{invnum} = $self->invnum;
1916 $cust_main->batch_card(%options);
1919 sub _agent_template {
1921 $self->cust_main->agent_template;
1924 sub _agent_invoice_from {
1926 $self->cust_main->agent_invoice_from;
1929 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1931 Returns an text invoice, as a list of lines.
1933 Options can be passed as a hashref (recommended) or as a list of time, template
1934 and then any key/value pairs for any other options.
1936 I<time>, if specified, is used to control the printing of overdue messages. The
1937 default is now. It isn't the date of the invoice; that's the `_date' field.
1938 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1939 L<Time::Local> and L<Date::Parse> for conversion functions.
1941 I<template>, if specified, is the name of a suffix for alternate invoices.
1943 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1949 my( $today, $template, %opt );
1951 %opt = %{ shift() };
1952 $today = delete($opt{'time'}) || '';
1953 $template = delete($opt{template}) || '';
1955 ( $today, $template, %opt ) = @_;
1958 my %params = ( 'format' => 'template' );
1959 $params{'time'} = $today if $today;
1960 $params{'template'} = $template if $template;
1961 $params{$_} = $opt{$_}
1962 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1964 $self->print_generic( %params );
1967 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1969 Internal method - returns a filename of a filled-in LaTeX template for this
1970 invoice (Note: add ".tex" to get the actual filename), and a filename of
1971 an associated logo (with the .eps extension included).
1973 See print_ps and print_pdf for methods that return PostScript and PDF output.
1975 Options can be passed as a hashref (recommended) or as a list of time, template
1976 and then any key/value pairs for any other options.
1978 I<time>, if specified, is used to control the printing of overdue messages. The
1979 default is now. It isn't the date of the invoice; that's the `_date' field.
1980 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1981 L<Time::Local> and L<Date::Parse> for conversion functions.
1983 I<template>, if specified, is the name of a suffix for alternate invoices.
1985 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1991 my( $today, $template, %opt );
1993 %opt = %{ shift() };
1994 $today = delete($opt{'time'}) || '';
1995 $template = delete($opt{template}) || '';
1997 ( $today, $template, %opt ) = @_;
2000 my %params = ( 'format' => 'latex' );
2001 $params{'time'} = $today if $today;
2002 $params{'template'} = $template if $template;
2003 $params{$_} = $opt{$_}
2004 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2006 $template ||= $self->_agent_template;
2008 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2009 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2013 ) or die "can't open temp file: $!\n";
2015 my $agentnum = $self->cust_main->agentnum;
2017 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2018 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2019 or die "can't write temp file: $!\n";
2021 print $lh $conf->config_binary('logo.eps', $agentnum)
2022 or die "can't write temp file: $!\n";
2025 $params{'logo_file'} = $lh->filename;
2027 my @filled_in = $self->print_generic( %params );
2029 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2033 ) or die "can't open temp file: $!\n";
2034 print $fh join('', @filled_in );
2037 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2038 return ($1, $params{'logo_file'});
2042 =item print_generic OPTION => VALUE ...
2044 Internal method - returns a filled-in template for this invoice as a scalar.
2046 See print_ps and print_pdf for methods that return PostScript and PDF output.
2048 Non optional options include
2049 format - latex, html, template
2051 Optional options include
2053 template - a value used as a suffix for a configuration template
2055 time - a value used to control the printing of overdue messages. The
2056 default is now. It isn't the date of the invoice; that's the `_date' field.
2057 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2058 L<Time::Local> and L<Date::Parse> for conversion functions.
2062 unsquelch_cdr - overrides any per customer cdr squelching when true
2064 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2068 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2069 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2072 my( $self, %params ) = @_;
2073 my $today = $params{today} ? $params{today} : time;
2074 warn "$me print_generic called on $self with suffix $params{template}\n"
2077 my $format = $params{format};
2078 die "Unknown format: $format"
2079 unless $format =~ /^(latex|html|template)$/;
2081 my $cust_main = $self->cust_main;
2082 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2083 unless $cust_main->payname
2084 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2086 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2087 'html' => [ '<%=', '%>' ],
2088 'template' => [ '{', '}' ],
2091 #create the template
2092 my $template = $params{template} ? $params{template} : $self->_agent_template;
2093 my $templatefile = "invoice_$format";
2094 $templatefile .= "_$template"
2095 if length($template);
2096 my @invoice_template = map "$_\n", $conf->config($templatefile)
2097 or die "cannot load config data $templatefile";
2100 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2101 #change this to a die when the old code is removed
2102 warn "old-style invoice template $templatefile; ".
2103 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2104 $old_latex = 'true';
2105 @invoice_template = _translate_old_latex_format(@invoice_template);
2108 my $text_template = new Text::Template(
2110 SOURCE => \@invoice_template,
2111 DELIMITERS => $delimiters{$format},
2114 $text_template->compile()
2115 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2118 # additional substitution could possibly cause breakage in existing templates
2119 my %convert_maps = (
2121 'notes' => sub { map "$_", @_ },
2122 'footer' => sub { map "$_", @_ },
2123 'smallfooter' => sub { map "$_", @_ },
2124 'returnaddress' => sub { map "$_", @_ },
2125 'coupon' => sub { map "$_", @_ },
2126 'summary' => sub { map "$_", @_ },
2132 s/%%(.*)$/<!-- $1 -->/g;
2133 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2134 s/\\begin\{enumerate\}/<ol>/g;
2136 s/\\end\{enumerate\}/<\/ol>/g;
2137 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2146 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2148 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2153 s/\\\\\*?\s*$/<BR>/;
2154 s/\\hyphenation\{[\w\s\-]+}//;
2159 'coupon' => sub { "" },
2160 'summary' => sub { "" },
2167 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2168 s/\\begin\{enumerate\}//g;
2170 s/\\end\{enumerate\}//g;
2171 s/\\textbf\{(.*)\}/$1/g;
2178 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2180 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2185 s/\\\\\*?\s*$/\n/; # dubious
2186 s/\\hyphenation\{[\w\s\-]+}//;
2190 'coupon' => sub { "" },
2191 'summary' => sub { "" },
2196 # hashes for differing output formats
2197 my %nbsps = ( 'latex' => '~',
2198 'html' => '', # '&nbps;' would be nice
2199 'template' => '', # not used
2201 my $nbsp = $nbsps{$format};
2203 my %escape_functions = ( 'latex' => \&_latex_escape,
2204 'html' => \&encode_entities,
2205 'template' => sub { shift },
2207 my $escape_function = $escape_functions{$format};
2209 my %date_formats = ( 'latex' => '%b %o, %Y',
2210 'html' => '%b %o, %Y',
2213 my $date_format = $date_formats{$format};
2215 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2217 'html' => sub { return '<b>'. shift(). '</b>'
2219 'template' => sub { shift },
2221 my $embolden_function = $embolden_functions{$format};
2224 # generate template variables
2227 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2231 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2237 $returnaddress = join("\n",
2238 $conf->config_orbase("invoice_${format}returnaddress", $template)
2241 } elsif ( grep /\S/,
2242 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2244 my $convert_map = $convert_maps{$format}{'returnaddress'};
2247 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2252 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2254 my $convert_map = $convert_maps{$format}{'returnaddress'};
2255 $returnaddress = join( "\n", &$convert_map(
2256 map { s/( {2,})/'~' x length($1)/eg;
2260 ( $conf->config('company_name', $self->cust_main->agentnum),
2261 $conf->config('company_address', $self->cust_main->agentnum),
2268 my $warning = "Couldn't find a return address; ".
2269 "do you need to set the company_address configuration value?";
2271 $returnaddress = $nbsp;
2272 #$returnaddress = $warning;
2276 my %invoice_data = (
2279 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2280 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2281 'returnaddress' => $returnaddress,
2282 'agent' => &$escape_function($cust_main->agent->agent),
2285 'invnum' => $self->invnum,
2286 'date' => time2str($date_format, $self->_date),
2287 'today' => time2str('%b %o, %Y', $today),
2288 'terms' => $self->terms,
2289 'template' => $template, #params{'template'},
2290 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2291 'current_charges' => sprintf("%.2f", $self->charged),
2292 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2295 'custnum' => $cust_main->display_custnum,
2296 'agent_custid' => &$escape_function($cust_main->agent_custid),
2297 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2298 payname company address1 address2 city state zip fax
2302 'ship_enable' => $conf->exists('invoice-ship_address'),
2303 'unitprices' => $conf->exists('invoice-unitprice'),
2304 'smallernotes' => $conf->exists('invoice-smallernotes'),
2305 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2307 # better hang on to conf_dir for a while (for old templates)
2308 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2310 #these are only used when doing paged plaintext
2316 $invoice_data{finance_section} = '';
2317 if ( $conf->config('finance_pkgclass') ) {
2319 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2320 $invoice_data{finance_section} = $pkg_class->categoryname;
2322 $invoice_data{finance_amount} = '0.00';
2324 my $countrydefault = $conf->config('countrydefault') || 'US';
2325 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2326 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2327 my $method = $prefix.$_;
2328 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2330 $invoice_data{'ship_country'} = ''
2331 if ( $invoice_data{'ship_country'} eq $countrydefault );
2333 $invoice_data{'cid'} = $params{'cid'}
2336 if ( $cust_main->country eq $countrydefault ) {
2337 $invoice_data{'country'} = '';
2339 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2343 $invoice_data{'address'} = \@address;
2345 $cust_main->payname.
2346 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2347 ? " (P.O. #". $cust_main->payinfo. ")"
2351 push @address, $cust_main->company
2352 if $cust_main->company;
2353 push @address, $cust_main->address1;
2354 push @address, $cust_main->address2
2355 if $cust_main->address2;
2357 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2358 push @address, $invoice_data{'country'}
2359 if $invoice_data{'country'};
2361 while (scalar(@address) < 5);
2363 $invoice_data{'logo_file'} = $params{'logo_file'}
2364 if $params{'logo_file'};
2366 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2367 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2368 #my $balance_due = $self->owed + $pr_total - $cr_total;
2369 my $balance_due = $self->owed + $pr_total;
2370 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2371 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2372 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2373 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2375 my $agentnum = $self->cust_main->agentnum;
2377 my $summarypage = '';
2378 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2381 $invoice_data{'summarypage'} = $summarypage;
2383 #do variable substitution in notes, footer, smallfooter
2384 foreach my $include (qw( notes footer smallfooter coupon )) {
2386 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2389 if ( $conf->exists($inc_file, $agentnum)
2390 && length( $conf->config($inc_file, $agentnum) ) ) {
2392 @inc_src = $conf->config($inc_file, $agentnum);
2396 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2398 my $convert_map = $convert_maps{$format}{$include};
2400 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2401 s/--\@\]/$delimiters{$format}[1]/g;
2404 &$convert_map( $conf->config($inc_file, $agentnum) );
2408 my $inc_tt = new Text::Template (
2410 SOURCE => [ map "$_\n", @inc_src ],
2411 DELIMITERS => $delimiters{$format},
2412 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2414 unless ( $inc_tt->compile() ) {
2415 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2416 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2420 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2422 $invoice_data{$include} =~ s/\n+$//
2423 if ($format eq 'latex');
2426 $invoice_data{'po_line'} =
2427 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2428 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2431 my %money_chars = ( 'latex' => '',
2432 'html' => $conf->config('money_char') || '$',
2435 my $money_char = $money_chars{$format};
2437 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2438 'html' => $conf->config('money_char') || '$',
2441 my $other_money_char = $other_money_chars{$format};
2442 $invoice_data{'dollar'} = $other_money_char;
2444 my @detail_items = ();
2445 my @total_items = ();
2449 $invoice_data{'detail_items'} = \@detail_items;
2450 $invoice_data{'total_items'} = \@total_items;
2451 $invoice_data{'buf'} = \@buf;
2452 $invoice_data{'sections'} = \@sections;
2454 my $previous_section = { 'description' => 'Previous Charges',
2455 'subtotal' => $other_money_char.
2456 sprintf('%.2f', $pr_total),
2457 'summarized' => $summarypage ? 'Y' : '',
2461 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2462 'subtotal' => $taxtotal, # adjusted below
2463 'summarized' => $summarypage ? 'Y' : '',
2466 my $adjusttotal = 0;
2467 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2468 'subtotal' => 0, # adjusted below
2469 'summarized' => $summarypage ? 'Y' : '',
2472 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2473 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2474 my $late_sections = [];
2475 if ( $multisection ) {
2477 $self->_items_sections( $late_sections, $summarypage, $escape_function );
2479 push @sections, { 'description' => '', 'subtotal' => '' };
2482 unless ( $conf->exists('disable_previous_balance')
2483 || $conf->exists('previous_balance-summary_only')
2487 foreach my $line_item ( $self->_items_previous ) {
2490 ext_description => [],
2492 $detail->{'ref'} = $line_item->{'pkgnum'};
2493 $detail->{'quantity'} = 1;
2494 $detail->{'section'} = $previous_section;
2495 $detail->{'description'} = &$escape_function($line_item->{'description'});
2496 if ( exists $line_item->{'ext_description'} ) {
2497 @{$detail->{'ext_description'}} = map {
2498 &$escape_function($_);
2499 } @{$line_item->{'ext_description'}};
2501 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2502 $line_item->{'amount'};
2503 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2505 push @detail_items, $detail;
2506 push @buf, [ $detail->{'description'},
2507 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2513 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2514 push @buf, ['','-----------'];
2515 push @buf, [ 'Total Previous Balance',
2516 $money_char. sprintf("%10.2f", $pr_total) ];
2520 foreach my $section (@sections, @$late_sections) {
2522 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2523 if ( $invoice_data{finance_section} &&
2524 $section->{'description'} eq $invoice_data{finance_section} );
2526 $section->{'subtotal'} = $other_money_char.
2527 sprintf('%.2f', $section->{'subtotal'})
2530 if ( $section->{'description'} ) {
2531 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2537 $options{'section'} = $section if $multisection;
2538 $options{'format'} = $format;
2539 $options{'escape_function'} = $escape_function;
2540 $options{'format_function'} = sub { () } unless $unsquelched;
2541 $options{'unsquelched'} = $unsquelched;
2542 $options{'summary_page'} = $summarypage;
2544 foreach my $line_item ( $self->_items_pkg(%options) ) {
2546 ext_description => [],
2548 $detail->{'ref'} = $line_item->{'pkgnum'};
2549 $detail->{'quantity'} = $line_item->{'quantity'};
2550 $detail->{'section'} = $section;
2551 $detail->{'description'} = &$escape_function($line_item->{'description'});
2552 if ( exists $line_item->{'ext_description'} ) {
2553 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2555 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2556 $line_item->{'amount'};
2557 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2558 $line_item->{'unit_amount'};
2559 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2561 push @detail_items, $detail;
2562 push @buf, ( [ $detail->{'description'},
2563 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2565 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2569 if ( $section->{'description'} ) {
2570 push @buf, ( ['','-----------'],
2571 [ $section->{'description'}. ' sub-total',
2572 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2581 $invoice_data{current_less_finance} =
2582 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2584 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2585 unshift @sections, $previous_section if $pr_total;
2588 foreach my $tax ( $self->_items_tax ) {
2590 $taxtotal += $tax->{'amount'};
2592 my $description = &$escape_function( $tax->{'description'} );
2593 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2595 if ( $multisection ) {
2597 my $money = $old_latex ? '' : $money_char;
2598 push @detail_items, {
2599 ext_description => [],
2602 description => $description,
2603 amount => $money. $amount,
2605 section => $tax_section,
2610 push @total_items, {
2611 'total_item' => $description,
2612 'total_amount' => $other_money_char. $amount,
2617 push @buf,[ $description,
2618 $money_char. $amount,
2625 $total->{'total_item'} = 'Sub-total';
2626 $total->{'total_amount'} =
2627 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2629 if ( $multisection ) {
2630 $tax_section->{'subtotal'} = $other_money_char.
2631 sprintf('%.2f', $taxtotal);
2632 $tax_section->{'pretotal'} = 'New charges sub-total '.
2633 $total->{'total_amount'};
2634 push @sections, $tax_section if $taxtotal;
2636 unshift @total_items, $total;
2639 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2641 push @buf,['','-----------'];
2642 push @buf,[( $conf->exists('disable_previous_balance')
2644 : 'Total New Charges'
2646 $money_char. sprintf("%10.2f",$self->charged) ];
2651 $total->{'total_item'} = &$embolden_function('Total');
2652 $total->{'total_amount'} =
2653 &$embolden_function(
2656 $self->charged + ( $conf->exists('disable_previous_balance')
2662 if ( $multisection ) {
2663 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2664 sprintf('%.2f', $self->charged );
2666 push @total_items, $total;
2668 push @buf,['','-----------'];
2669 push @buf,['Total Charges',
2671 sprintf( '%10.2f', $self->charged +
2672 ( $conf->exists('disable_previous_balance')
2681 unless ( $conf->exists('disable_previous_balance') ) {
2682 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2685 my $credittotal = 0;
2686 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2689 $total->{'total_item'} = &$escape_function($credit->{'description'});
2690 $credittotal += $credit->{'amount'};
2691 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2692 $adjusttotal += $credit->{'amount'};
2693 if ( $multisection ) {
2694 my $money = $old_latex ? '' : $money_char;
2695 push @detail_items, {
2696 ext_description => [],
2699 description => &$escape_function($credit->{'description'}),
2700 amount => $money. $credit->{'amount'},
2702 section => $adjust_section,
2705 push @total_items, $total;
2709 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2712 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2713 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2717 my $paymenttotal = 0;
2718 foreach my $payment ( $self->_items_payments ) {
2720 $total->{'total_item'} = &$escape_function($payment->{'description'});
2721 $paymenttotal += $payment->{'amount'};
2722 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2723 $adjusttotal += $payment->{'amount'};
2724 if ( $multisection ) {
2725 my $money = $old_latex ? '' : $money_char;
2726 push @detail_items, {
2727 ext_description => [],
2730 description => &$escape_function($payment->{'description'}),
2731 amount => $money. $payment->{'amount'},
2733 section => $adjust_section,
2736 push @total_items, $total;
2738 push @buf, [ $payment->{'description'},
2739 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2742 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2744 if ( $multisection ) {
2745 $adjust_section->{'subtotal'} = $other_money_char.
2746 sprintf('%.2f', $adjusttotal);
2747 push @sections, $adjust_section;
2752 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2753 $total->{'total_amount'} =
2754 &$embolden_function(
2755 $other_money_char. sprintf('%.2f', $summarypage
2757 $self->billing_balance
2758 : $self->owed + $pr_total
2761 if ( $multisection ) {
2762 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2763 $total->{'total_amount'};
2765 push @total_items, $total;
2767 push @buf,['','-----------'];
2768 push @buf,[$self->balance_due_msg, $money_char.
2769 sprintf("%10.2f", $balance_due ) ];
2773 if ( $multisection ) {
2774 push @sections, @$late_sections
2778 my @includelist = ();
2779 push @includelist, 'summary' if $summarypage;
2780 foreach my $include ( @includelist ) {
2782 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2785 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2787 @inc_src = $conf->config($inc_file, $agentnum);
2791 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2793 my $convert_map = $convert_maps{$format}{$include};
2795 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2796 s/--\@\]/$delimiters{$format}[1]/g;
2799 &$convert_map( $conf->config($inc_file, $agentnum) );
2803 my $inc_tt = new Text::Template (
2805 SOURCE => [ map "$_\n", @inc_src ],
2806 DELIMITERS => $delimiters{$format},
2807 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2809 unless ( $inc_tt->compile() ) {
2810 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2811 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2815 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2817 $invoice_data{$include} =~ s/\n+$//
2818 if ($format eq 'latex');
2823 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2824 /invoice_lines\((\d*)\)/;
2825 $invoice_lines += $1 || scalar(@buf);
2828 die "no invoice_lines() functions in template?"
2829 if ( $format eq 'template' && !$wasfunc );
2831 if ($format eq 'template') {
2833 if ( $invoice_lines ) {
2834 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2835 $invoice_data{'total_pages'}++
2836 if scalar(@buf) % $invoice_lines;
2839 #setup subroutine for the template
2840 sub FS::cust_bill::_template::invoice_lines {
2841 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2843 scalar(@FS::cust_bill::_template::buf)
2844 ? shift @FS::cust_bill::_template::buf
2853 push @collect, split("\n",
2854 $text_template->fill_in( HASH => \%invoice_data,
2855 PACKAGE => 'FS::cust_bill::_template'
2858 $FS::cust_bill::_template::page++;
2860 map "$_\n", @collect;
2862 warn "filling in template for invoice ". $self->invnum. "\n"
2864 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2867 $text_template->fill_in(HASH => \%invoice_data);
2871 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2873 Returns an postscript invoice, as a scalar.
2875 Options can be passed as a hashref (recommended) or as a list of time, template
2876 and then any key/value pairs for any other options.
2878 I<time> an optional value used to control the printing of overdue messages. The
2879 default is now. It isn't the date of the invoice; that's the `_date' field.
2880 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2881 L<Time::Local> and L<Date::Parse> for conversion functions.
2883 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2890 my ($file, $lfile) = $self->print_latex(@_);
2891 my $ps = generate_ps($file);
2897 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2899 Returns an PDF invoice, as a scalar.
2901 Options can be passed as a hashref (recommended) or as a list of time, template
2902 and then any key/value pairs for any other options.
2904 I<time> an optional value used to control the printing of overdue messages. The
2905 default is now. It isn't the date of the invoice; that's the `_date' field.
2906 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2907 L<Time::Local> and L<Date::Parse> for conversion functions.
2909 I<template>, if specified, is the name of a suffix for alternate invoices.
2911 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2918 my ($file, $lfile) = $self->print_latex(@_);
2919 my $pdf = generate_pdf($file);
2925 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2927 Returns an HTML invoice, as a scalar.
2929 I<time> an optional value used to control the printing of overdue messages. The
2930 default is now. It isn't the date of the invoice; that's the `_date' field.
2931 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2932 L<Time::Local> and L<Date::Parse> for conversion functions.
2934 I<template>, if specified, is the name of a suffix for alternate invoices.
2936 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2938 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2939 when emailing the invoice as part of a multipart/related MIME email.
2947 %params = %{ shift() };
2949 $params{'time'} = shift;
2950 $params{'template'} = shift;
2951 $params{'cid'} = shift;
2954 $params{'format'} = 'html';
2956 $self->print_generic( %params );
2959 # quick subroutine for print_latex
2961 # There are ten characters that LaTeX treats as special characters, which
2962 # means that they do not simply typeset themselves:
2963 # # $ % & ~ _ ^ \ { }
2965 # TeX ignores blanks following an escaped character; if you want a blank (as
2966 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2970 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2971 $value =~ s/([<>])/\$$1\$/g;
2975 #utility methods for print_*
2977 sub _translate_old_latex_format {
2978 warn "_translate_old_latex_format called\n"
2985 if ( $line =~ /^%%Detail\s*$/ ) {
2987 push @template, q![@--!,
2988 q! foreach my $_tr_line (@detail_items) {!,
2989 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2990 q! $_tr_line->{'description'} .= !,
2991 q! "\\tabularnewline\n~~".!,
2992 q! join( "\\tabularnewline\n~~",!,
2993 q! @{$_tr_line->{'ext_description'}}!,
2997 while ( ( my $line_item_line = shift )
2998 !~ /^%%EndDetail\s*$/ ) {
2999 $line_item_line =~ s/'/\\'/g; # nice LTS
3000 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3001 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3002 push @template, " \$OUT .= '$line_item_line';";
3005 push @template, '}',
3008 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3010 push @template, '[@--',
3011 ' foreach my $_tr_line (@total_items) {';
3013 while ( ( my $total_item_line = shift )
3014 !~ /^%%EndTotalDetails\s*$/ ) {
3015 $total_item_line =~ s/'/\\'/g; # nice LTS
3016 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3017 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3018 push @template, " \$OUT .= '$total_item_line';";
3021 push @template, '}',
3025 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3026 push @template, $line;
3032 warn "$_\n" foreach @template;
3041 #check for an invoice-specific override
3042 return $self->invoice_terms if $self->invoice_terms;
3044 #check for a customer- specific override
3045 my $cust_main = $self->cust_main;
3046 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3048 #use configured default
3049 $conf->config('invoice_default_terms') || '';
3055 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3056 $duedate = $self->_date() + ( $1 * 86400 );
3063 $self->due_date ? time2str(shift, $self->due_date) : '';
3066 sub balance_due_msg {
3068 my $msg = 'Balance Due';
3069 return $msg unless $self->terms;
3070 if ( $self->due_date ) {
3071 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3072 } elsif ( $self->terms ) {
3073 $msg .= ' - '. $self->terms;
3078 sub balance_due_date {
3081 if ( $conf->exists('invoice_default_terms')
3082 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3083 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3088 =item invnum_date_pretty
3090 Returns a string with the invoice number and date, for example:
3091 "Invoice #54 (3/20/2008)"
3095 sub invnum_date_pretty {
3097 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3102 Returns a string with the date, for example: "3/20/2008"
3108 time2str('%x', $self->_date);
3111 sub _items_sections {
3114 my $summarypage = shift;
3121 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3125 my $usage = $cust_bill_pkg->usage;
3127 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3128 next if ( $display->summary && $summarypage );
3130 my $desc = $display->section;
3131 my $type = $display->type;
3133 if ( $cust_bill_pkg->pkgnum > 0 ) {
3134 $not_tax{$desc} = 1;
3137 if ( $display->post_total && !$summarypage ) {
3138 if (! $type || $type eq 'S') {
3139 $l{$desc} += $cust_bill_pkg->setup
3140 if ( $cust_bill_pkg->setup != 0 );
3144 $l{$desc} += $cust_bill_pkg->recur
3145 if ( $cust_bill_pkg->recur != 0 );
3148 if ($type && $type eq 'R') {
3149 $l{$desc} += $cust_bill_pkg->recur - $usage
3150 if ( $cust_bill_pkg->recur != 0 );
3153 if ($type && $type eq 'U') {
3154 $l{$desc} += $usage;
3158 if (! $type || $type eq 'S') {
3159 $s{$desc} += $cust_bill_pkg->setup
3160 if ( $cust_bill_pkg->setup != 0 );
3164 $s{$desc} += $cust_bill_pkg->recur
3165 if ( $cust_bill_pkg->recur != 0 );
3168 if ($type && $type eq 'R') {
3169 $s{$desc} += $cust_bill_pkg->recur - $usage
3170 if ( $cust_bill_pkg->recur != 0 );
3173 if ($type && $type eq 'U') {
3174 $s{$desc} += $usage;
3183 my %cache = map { $_->categoryname => $_ }
3184 qsearch( 'pkg_category', {disabled => 'Y'} );
3185 $cache{$_->categoryname} = $_
3186 foreach qsearch( 'pkg_category', {disabled => ''} );
3188 push @$late, map { { 'description' => &{$escape}($_),
3189 'subtotal' => $l{$_},
3192 sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
3194 map { { 'description' => &{$escape}($_),
3195 'subtotal' => $s{$_},
3196 'summarized' => $not_tax{$_} ? '' : 'Y',
3197 'tax_section' => $not_tax{$_} ? '' : 'Y',
3199 sort { $cache{$a}->weight <=> $cache{$b}->weight }
3201 ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
3210 #my @display = scalar(@_)
3212 # : qw( _items_previous _items_pkg );
3213 # #: qw( _items_pkg );
3214 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3215 my @display = qw( _items_previous _items_pkg );
3218 foreach my $display ( @display ) {
3219 push @b, $self->$display(@_);
3224 sub _items_previous {
3226 my $cust_main = $self->cust_main;
3227 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3229 foreach ( @pr_cust_bill ) {
3231 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3232 ' ('. time2str('%x',$_->_date). ')',
3233 #'pkgpart' => 'N/A',
3235 'amount' => sprintf("%.2f", $_->owed),
3241 # 'description' => 'Previous Balance',
3242 # #'pkgpart' => 'N/A',
3243 # 'pkgnum' => 'N/A',
3244 # 'amount' => sprintf("%10.2f", $pr_total ),
3245 # 'ext_description' => [ map {
3246 # "Invoice ". $_->invnum.
3247 # " (". time2str("%x",$_->_date). ") ".
3248 # sprintf("%10.2f", $_->owed)
3249 # } @pr_cust_bill ],
3256 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3257 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3261 return 0 unless $a cmp $b;
3262 return -1 if $b eq 'Tax';
3263 return 1 if $a eq 'Tax';
3264 return -1 if $b eq 'Other surcharges';
3265 return 1 if $a eq 'Other surcharges';
3271 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3272 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3275 sub _items_cust_bill_pkg {
3277 my $cust_bill_pkg = shift;
3280 my $format = $opt{format} || '';
3281 my $escape_function = $opt{escape_function} || sub { shift };
3282 my $format_function = $opt{format_function} || '';
3283 my $unsquelched = $opt{unsquelched} || '';
3284 my $section = $opt{section}->{description} if $opt{section};
3285 my $summary_page = $opt{summary_page} || '';
3288 my ($s, $r, $u) = ( undef, undef, undef );
3289 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3292 foreach ( $s, $r, $u ) {
3293 if ( $_ && !$cust_bill_pkg->hidden ) {
3294 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3295 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3301 foreach my $display ( grep { defined($section)
3302 ? $_->section eq $section
3305 grep { $_->summary || !$summary_page }
3306 $cust_bill_pkg->cust_bill_pkg_display
3310 my $type = $display->type;
3312 my $desc = $cust_bill_pkg->desc;
3313 $desc = substr($desc, 0, 50). '...'
3314 if $format eq 'latex' && length($desc) > 50;
3316 my %details_opt = ( 'format' => $format,
3317 'escape_function' => $escape_function,
3318 'format_function' => $format_function,
3321 if ( $cust_bill_pkg->pkgnum > 0 ) {
3323 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3325 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3327 my $description = $desc;
3328 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3331 push @d, map &{$escape_function}($_),
3332 $cust_pkg->h_labels_short($self->_date)
3333 unless $cust_pkg->part_pkg->hide_svc_detail
3334 || $cust_bill_pkg->hidden;
3335 push @d, $cust_bill_pkg->details(%details_opt)
3336 if $cust_bill_pkg->recur == 0;
3338 if ( $cust_bill_pkg->hidden ) {
3339 $s->{amount} += $cust_bill_pkg->setup;
3340 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3341 push @{ $s->{ext_description} }, @d;
3344 description => $description,
3345 #pkgpart => $part_pkg->pkgpart,
3346 pkgnum => $cust_bill_pkg->pkgnum,
3347 amount => $cust_bill_pkg->setup,
3348 unit_amount => $cust_bill_pkg->unitsetup,
3349 quantity => $cust_bill_pkg->quantity,
3350 ext_description => \@d,
3356 if ( $cust_bill_pkg->recur != 0 &&
3357 ( !$type || $type eq 'R' || $type eq 'U' )
3361 my $is_summary = $display->summary;
3362 my $description = ($is_summary && $type && $type eq 'U')
3363 ? "Usage charges" : $desc;
3365 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3366 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3367 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3372 #at least until cust_bill_pkg has "past" ranges in addition to
3373 #the "future" sdate/edate ones... see #3032
3374 my @dates = ( $self->_date );
3375 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3376 push @dates, $prev->sdate if $prev;
3378 push @d, map &{$escape_function}($_),
3379 $cust_pkg->h_labels_short(@dates)
3380 #$cust_bill_pkg->edate,
3381 #$cust_bill_pkg->sdate)
3382 unless $cust_pkg->part_pkg->hide_svc_detail
3383 || $cust_bill_pkg->itemdesc
3384 || $cust_bill_pkg->hidden
3385 || $is_summary && $type && $type eq 'U';
3387 push @d, $cust_bill_pkg->details(%details_opt)
3388 unless ($is_summary || $type && $type eq 'R');
3392 $amount = $cust_bill_pkg->recur;
3393 }elsif($type eq 'R') {
3394 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3395 }elsif($type eq 'U') {
3396 $amount = $cust_bill_pkg->usage;
3399 if ( !$type || $type eq 'R' ) {
3401 if ( $cust_bill_pkg->hidden ) {
3402 $r->{amount} += $amount;
3403 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3404 push @{ $r->{ext_description} }, @d;
3407 description => $description,
3408 #pkgpart => $part_pkg->pkgpart,
3409 pkgnum => $cust_bill_pkg->pkgnum,
3411 unit_amount => $cust_bill_pkg->unitrecur,
3412 quantity => $cust_bill_pkg->quantity,
3413 ext_description => \@d,
3417 } elsif ( $amount ) { # && $type eq 'U'
3419 if ( $cust_bill_pkg->hidden ) {
3420 $u->{amount} += $amount;
3421 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3422 push @{ $u->{ext_description} }, @d;
3425 description => $description,
3426 #pkgpart => $part_pkg->pkgpart,
3427 pkgnum => $cust_bill_pkg->pkgnum,
3429 unit_amount => $cust_bill_pkg->unitrecur,
3430 quantity => $cust_bill_pkg->quantity,
3431 ext_description => \@d,
3437 } # recurring or usage with recurring charge
3439 } else { #pkgnum tax or one-shot line item (??)
3441 if ( $cust_bill_pkg->setup != 0 ) {
3443 'description' => $desc,
3444 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3447 if ( $cust_bill_pkg->recur != 0 ) {
3449 'description' => "$desc (".
3450 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3451 time2str("%x", $cust_bill_pkg->edate). ')',
3452 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3462 foreach ( $s, $r, $u ) {
3464 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3465 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3474 sub _items_credits {
3475 my( $self, %opt ) = @_;
3476 my $trim_len = $opt{'trim_len'} || 60;
3480 foreach ( $self->cust_credited ) {
3482 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3484 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3485 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3486 $reason = " ($reason) " if $reason;
3489 #'description' => 'Credit ref\#'. $_->crednum.
3490 # " (". time2str("%x",$_->cust_credit->_date) .")".
3492 'description' => 'Credit applied '.
3493 time2str("%x",$_->cust_credit->_date). $reason,
3494 'amount' => sprintf("%.2f",$_->amount),
3502 sub _items_payments {
3506 #get & print payments
3507 foreach ( $self->cust_bill_pay ) {
3509 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3512 'description' => "Payment received ".
3513 time2str("%x",$_->cust_pay->_date ),
3514 'amount' => sprintf("%.2f", $_->amount )
3522 =item call_details [ OPTION => VALUE ... ]
3524 Returns an array of CSV strings representing the call details for this invoice
3525 The only option available is the boolean prepend_billed_number
3530 my ($self, %opt) = @_;
3532 my $format_function = sub { shift };
3534 if ($opt{prepend_billed_number}) {
3535 $format_function = sub {
3539 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3544 my @details = map { $_->details( 'format_function' => $format_function,
3545 'escape_function' => sub{ return() },
3549 $self->cust_bill_pkg;
3550 my $header = $details[0];
3551 ( $header, grep { $_ ne $header } @details );
3561 =item process_reprint
3565 sub process_reprint {
3566 process_re_X('print', @_);
3569 =item process_reemail
3573 sub process_reemail {
3574 process_re_X('email', @_);
3582 process_re_X('fax', @_);
3590 process_re_X('ftp', @_);
3597 sub process_respool {
3598 process_re_X('spool', @_);
3601 use Storable qw(thaw);
3605 my( $method, $job ) = ( shift, shift );
3606 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3608 my $param = thaw(decode_base64(shift));
3609 warn Dumper($param) if $DEBUG;
3620 my($method, $job, %param ) = @_;
3622 warn "re_X $method for job $job with param:\n".
3623 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3626 #some false laziness w/search/cust_bill.html
3628 my $orderby = 'ORDER BY cust_bill._date';
3630 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3632 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3634 my @cust_bill = qsearch( {
3635 #'select' => "cust_bill.*",
3636 'table' => 'cust_bill',
3637 'addl_from' => $addl_from,
3639 'extra_sql' => $extra_sql,
3640 'order_by' => $orderby,
3644 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3646 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3649 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3650 foreach my $cust_bill ( @cust_bill ) {
3651 $cust_bill->$method();
3653 if ( $job ) { #progressbar foo
3655 if ( time - $min_sec > $last ) {
3656 my $error = $job->update_statustext(
3657 int( 100 * $num / scalar(@cust_bill) )
3659 die $error if $error;
3670 =head1 CLASS METHODS
3676 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3682 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3687 Returns an SQL fragment to retreive the net amount (charged minus credited).
3693 'charged - '. $class->credited_sql;
3698 Returns an SQL fragment to retreive the amount paid against this invoice.
3704 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3705 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3710 Returns an SQL fragment to retreive the amount credited against this invoice.
3716 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3717 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3720 =item search_sql HASHREF
3722 Class method which returns an SQL WHERE fragment to search for parameters
3723 specified in HASHREF. Valid parameters are
3729 Epoch date (UNIX timestamp) setting a lower bound for _date values
3733 Epoch date (UNIX timestamp) setting an upper bound for _date values
3747 =item newest_percust
3751 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3756 my($class, $param) = @_;
3758 warn "$me search_sql called with params: \n".
3759 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3764 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3765 push @search, "cust_bill._date >= $1";
3767 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3768 push @search, "cust_bill._date < $1";
3770 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3771 push @search, "cust_bill.invnum >= $1";
3773 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3774 push @search, "cust_bill.invnum <= $1";
3776 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3777 push @search, "cust_main.agentnum = $1";
3780 push @search, '0 != '. FS::cust_bill->owed_sql
3781 if $param->{'open'};
3783 push @search, '0 != '. FS::cust_bill->net_sql
3786 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3787 if $param->{'days'};
3789 if ( $param->{'newest_percust'} ) {
3791 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3792 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3794 my @newest_where = map { my $x = $_;
3795 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3798 grep ! /^cust_main./, @search;
3799 my $newest_where = scalar(@newest_where)
3800 ? ' AND '. join(' AND ', @newest_where)
3804 push @search, "cust_bill._date = (
3805 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3806 WHERE newest_cust_bill.custnum = cust_bill.custnum
3812 my $curuser = $FS::CurrentUser::CurrentUser;
3813 if ( $curuser->username eq 'fs_queue'
3814 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3816 my $newuser = qsearchs('access_user', {
3817 'username' => $username,
3821 $curuser = $newuser;
3823 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3827 push @search, $curuser->agentnums_sql;
3829 join(' AND ', @search );
3841 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3842 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base