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' ?
2070 # yes: fixed width (dot matrix) text printing will be borked
2073 my( $self, %params ) = @_;
2074 my $today = $params{today} ? $params{today} : time;
2075 warn "$me print_generic called on $self with suffix $params{template}\n"
2078 my $format = $params{format};
2079 die "Unknown format: $format"
2080 unless $format =~ /^(latex|html|template)$/;
2082 my $cust_main = $self->cust_main;
2083 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2084 unless $cust_main->payname
2085 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2087 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2088 'html' => [ '<%=', '%>' ],
2089 'template' => [ '{', '}' ],
2092 #create the template
2093 my $template = $params{template} ? $params{template} : $self->_agent_template;
2094 my $templatefile = "invoice_$format";
2095 $templatefile .= "_$template"
2096 if length($template);
2097 my @invoice_template = map "$_\n", $conf->config($templatefile)
2098 or die "cannot load config data $templatefile";
2101 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2102 #change this to a die when the old code is removed
2103 warn "old-style invoice template $templatefile; ".
2104 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2105 $old_latex = 'true';
2106 @invoice_template = _translate_old_latex_format(@invoice_template);
2109 my $text_template = new Text::Template(
2111 SOURCE => \@invoice_template,
2112 DELIMITERS => $delimiters{$format},
2115 $text_template->compile()
2116 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2119 # additional substitution could possibly cause breakage in existing templates
2120 my %convert_maps = (
2122 'notes' => sub { map "$_", @_ },
2123 'footer' => sub { map "$_", @_ },
2124 'smallfooter' => sub { map "$_", @_ },
2125 'returnaddress' => sub { map "$_", @_ },
2126 'coupon' => sub { map "$_", @_ },
2127 'summary' => sub { map "$_", @_ },
2133 s/%%(.*)$/<!-- $1 -->/g;
2134 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2135 s/\\begin\{enumerate\}/<ol>/g;
2137 s/\\end\{enumerate\}/<\/ol>/g;
2138 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2147 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2149 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2154 s/\\\\\*?\s*$/<BR>/;
2155 s/\\hyphenation\{[\w\s\-]+}//;
2160 'coupon' => sub { "" },
2161 'summary' => sub { "" },
2168 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2169 s/\\begin\{enumerate\}//g;
2171 s/\\end\{enumerate\}//g;
2172 s/\\textbf\{(.*)\}/$1/g;
2179 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2181 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2186 s/\\\\\*?\s*$/\n/; # dubious
2187 s/\\hyphenation\{[\w\s\-]+}//;
2191 'coupon' => sub { "" },
2192 'summary' => sub { "" },
2197 # hashes for differing output formats
2198 my %nbsps = ( 'latex' => '~',
2199 'html' => '', # '&nbps;' would be nice
2200 'template' => '', # not used
2202 my $nbsp = $nbsps{$format};
2204 my %escape_functions = ( 'latex' => \&_latex_escape,
2205 'html' => \&encode_entities,
2206 'template' => sub { shift },
2208 my $escape_function = $escape_functions{$format};
2210 my %date_formats = ( 'latex' => '%b %o, %Y',
2211 'html' => '%b %o, %Y',
2214 my $date_format = $date_formats{$format};
2216 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2218 'html' => sub { return '<b>'. shift(). '</b>'
2220 'template' => sub { shift },
2222 my $embolden_function = $embolden_functions{$format};
2225 # generate template variables
2228 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2232 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2238 $returnaddress = join("\n",
2239 $conf->config_orbase("invoice_${format}returnaddress", $template)
2242 } elsif ( grep /\S/,
2243 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2245 my $convert_map = $convert_maps{$format}{'returnaddress'};
2248 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2253 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2255 my $convert_map = $convert_maps{$format}{'returnaddress'};
2256 $returnaddress = join( "\n", &$convert_map(
2257 map { s/( {2,})/'~' x length($1)/eg;
2261 ( $conf->config('company_name', $self->cust_main->agentnum),
2262 $conf->config('company_address', $self->cust_main->agentnum),
2269 my $warning = "Couldn't find a return address; ".
2270 "do you need to set the company_address configuration value?";
2272 $returnaddress = $nbsp;
2273 #$returnaddress = $warning;
2277 my %invoice_data = (
2280 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2281 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2282 'returnaddress' => $returnaddress,
2283 'agent' => &$escape_function($cust_main->agent->agent),
2286 'invnum' => $self->invnum,
2287 'date' => time2str($date_format, $self->_date),
2288 'today' => time2str('%b %o, %Y', $today),
2289 'terms' => $self->terms,
2290 'template' => $template, #params{'template'},
2291 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2292 'current_charges' => sprintf("%.2f", $self->charged),
2293 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2296 'custnum' => $cust_main->display_custnum,
2297 'agent_custid' => &$escape_function($cust_main->agent_custid),
2298 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2299 payname company address1 address2 city state zip fax
2303 'ship_enable' => $conf->exists('invoice-ship_address'),
2304 'unitprices' => $conf->exists('invoice-unitprice'),
2305 'smallernotes' => $conf->exists('invoice-smallernotes'),
2306 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2308 # better hang on to conf_dir for a while (for old templates)
2309 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2311 #these are only used when doing paged plaintext
2317 $invoice_data{finance_section} = '';
2318 if ( $conf->config('finance_pkgclass') ) {
2320 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2321 $invoice_data{finance_section} = $pkg_class->categoryname;
2323 $invoice_data{finance_amount} = '0.00';
2325 my $countrydefault = $conf->config('countrydefault') || 'US';
2326 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2327 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2328 my $method = $prefix.$_;
2329 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2331 $invoice_data{'ship_country'} = ''
2332 if ( $invoice_data{'ship_country'} eq $countrydefault );
2334 $invoice_data{'cid'} = $params{'cid'}
2337 if ( $cust_main->country eq $countrydefault ) {
2338 $invoice_data{'country'} = '';
2340 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2344 $invoice_data{'address'} = \@address;
2346 $cust_main->payname.
2347 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2348 ? " (P.O. #". $cust_main->payinfo. ")"
2352 push @address, $cust_main->company
2353 if $cust_main->company;
2354 push @address, $cust_main->address1;
2355 push @address, $cust_main->address2
2356 if $cust_main->address2;
2358 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2359 push @address, $invoice_data{'country'}
2360 if $invoice_data{'country'};
2362 while (scalar(@address) < 5);
2364 $invoice_data{'logo_file'} = $params{'logo_file'}
2365 if $params{'logo_file'};
2367 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2368 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2369 #my $balance_due = $self->owed + $pr_total - $cr_total;
2370 my $balance_due = $self->owed + $pr_total;
2371 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2372 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2373 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2374 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2376 my $agentnum = $self->cust_main->agentnum;
2378 my $summarypage = '';
2379 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2382 $invoice_data{'summarypage'} = $summarypage;
2384 #do variable substitution in notes, footer, smallfooter
2385 foreach my $include (qw( notes footer smallfooter coupon )) {
2387 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2390 if ( $conf->exists($inc_file, $agentnum)
2391 && length( $conf->config($inc_file, $agentnum) ) ) {
2393 @inc_src = $conf->config($inc_file, $agentnum);
2397 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2399 my $convert_map = $convert_maps{$format}{$include};
2401 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2402 s/--\@\]/$delimiters{$format}[1]/g;
2405 &$convert_map( $conf->config($inc_file, $agentnum) );
2409 my $inc_tt = new Text::Template (
2411 SOURCE => [ map "$_\n", @inc_src ],
2412 DELIMITERS => $delimiters{$format},
2413 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2415 unless ( $inc_tt->compile() ) {
2416 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2417 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2421 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2423 $invoice_data{$include} =~ s/\n+$//
2424 if ($format eq 'latex');
2427 $invoice_data{'po_line'} =
2428 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2429 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2432 my %money_chars = ( 'latex' => '',
2433 'html' => $conf->config('money_char') || '$',
2436 my $money_char = $money_chars{$format};
2438 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2439 'html' => $conf->config('money_char') || '$',
2442 my $other_money_char = $other_money_chars{$format};
2443 $invoice_data{'dollar'} = $other_money_char;
2445 my @detail_items = ();
2446 my @total_items = ();
2450 $invoice_data{'detail_items'} = \@detail_items;
2451 $invoice_data{'total_items'} = \@total_items;
2452 $invoice_data{'buf'} = \@buf;
2453 $invoice_data{'sections'} = \@sections;
2455 my $previous_section = { 'description' => 'Previous Charges',
2456 'subtotal' => $other_money_char.
2457 sprintf('%.2f', $pr_total),
2458 'summarized' => $summarypage ? 'Y' : '',
2462 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2463 'subtotal' => $taxtotal, # adjusted below
2464 'summarized' => $summarypage ? 'Y' : '',
2467 my $adjusttotal = 0;
2468 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2469 'subtotal' => 0, # adjusted below
2470 'summarized' => $summarypage ? 'Y' : '',
2473 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2474 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2475 my $late_sections = [];
2476 if ( $multisection ) {
2478 $self->_items_sections( $late_sections, $summarypage, $escape_function );
2480 push @sections, { 'description' => '', 'subtotal' => '' };
2483 unless ( $conf->exists('disable_previous_balance')
2484 || $conf->exists('previous_balance-summary_only')
2488 foreach my $line_item ( $self->_items_previous ) {
2491 ext_description => [],
2493 $detail->{'ref'} = $line_item->{'pkgnum'};
2494 $detail->{'quantity'} = 1;
2495 $detail->{'section'} = $previous_section;
2496 $detail->{'description'} = &$escape_function($line_item->{'description'});
2497 if ( exists $line_item->{'ext_description'} ) {
2498 @{$detail->{'ext_description'}} = map {
2499 &$escape_function($_);
2500 } @{$line_item->{'ext_description'}};
2502 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2503 $line_item->{'amount'};
2504 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2506 push @detail_items, $detail;
2507 push @buf, [ $detail->{'description'},
2508 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2514 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2515 push @buf, ['','-----------'];
2516 push @buf, [ 'Total Previous Balance',
2517 $money_char. sprintf("%10.2f", $pr_total) ];
2521 foreach my $section (@sections, @$late_sections) {
2523 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2524 if ( $invoice_data{finance_section} &&
2525 $section->{'description'} eq $invoice_data{finance_section} );
2527 $section->{'subtotal'} = $other_money_char.
2528 sprintf('%.2f', $section->{'subtotal'})
2531 if ( $section->{'description'} ) {
2532 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2538 $options{'section'} = $section if $multisection;
2539 $options{'format'} = $format;
2540 $options{'escape_function'} = $escape_function;
2541 $options{'format_function'} = sub { () } unless $unsquelched;
2542 $options{'unsquelched'} = $unsquelched;
2543 $options{'summary_page'} = $summarypage;
2545 foreach my $line_item ( $self->_items_pkg(%options) ) {
2547 ext_description => [],
2549 $detail->{'ref'} = $line_item->{'pkgnum'};
2550 $detail->{'quantity'} = $line_item->{'quantity'};
2551 $detail->{'section'} = $section;
2552 $detail->{'description'} = &$escape_function($line_item->{'description'});
2553 if ( exists $line_item->{'ext_description'} ) {
2554 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2556 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2557 $line_item->{'amount'};
2558 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2559 $line_item->{'unit_amount'};
2560 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2562 push @detail_items, $detail;
2563 push @buf, ( [ $detail->{'description'},
2564 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2566 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2570 if ( $section->{'description'} ) {
2571 push @buf, ( ['','-----------'],
2572 [ $section->{'description'}. ' sub-total',
2573 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2582 $invoice_data{current_less_finance} =
2583 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2585 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2586 unshift @sections, $previous_section if $pr_total;
2589 foreach my $tax ( $self->_items_tax ) {
2591 $taxtotal += $tax->{'amount'};
2593 my $description = &$escape_function( $tax->{'description'} );
2594 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2596 if ( $multisection ) {
2598 my $money = $old_latex ? '' : $money_char;
2599 push @detail_items, {
2600 ext_description => [],
2603 description => $description,
2604 amount => $money. $amount,
2606 section => $tax_section,
2611 push @total_items, {
2612 'total_item' => $description,
2613 'total_amount' => $other_money_char. $amount,
2618 push @buf,[ $description,
2619 $money_char. $amount,
2626 $total->{'total_item'} = 'Sub-total';
2627 $total->{'total_amount'} =
2628 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2630 if ( $multisection ) {
2631 $tax_section->{'subtotal'} = $other_money_char.
2632 sprintf('%.2f', $taxtotal);
2633 $tax_section->{'pretotal'} = 'New charges sub-total '.
2634 $total->{'total_amount'};
2635 push @sections, $tax_section if $taxtotal;
2637 unshift @total_items, $total;
2640 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2642 push @buf,['','-----------'];
2643 push @buf,[( $conf->exists('disable_previous_balance')
2645 : 'Total New Charges'
2647 $money_char. sprintf("%10.2f",$self->charged) ];
2652 $total->{'total_item'} = &$embolden_function('Total');
2653 $total->{'total_amount'} =
2654 &$embolden_function(
2657 $self->charged + ( $conf->exists('disable_previous_balance')
2663 if ( $multisection ) {
2664 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2665 sprintf('%.2f', $self->charged );
2667 push @total_items, $total;
2669 push @buf,['','-----------'];
2670 push @buf,['Total Charges',
2672 sprintf( '%10.2f', $self->charged +
2673 ( $conf->exists('disable_previous_balance')
2682 unless ( $conf->exists('disable_previous_balance') ) {
2683 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2686 my $credittotal = 0;
2687 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2690 $total->{'total_item'} = &$escape_function($credit->{'description'});
2691 $credittotal += $credit->{'amount'};
2692 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2693 $adjusttotal += $credit->{'amount'};
2694 if ( $multisection ) {
2695 my $money = $old_latex ? '' : $money_char;
2696 push @detail_items, {
2697 ext_description => [],
2700 description => &$escape_function($credit->{'description'}),
2701 amount => $money. $credit->{'amount'},
2703 section => $adjust_section,
2706 push @total_items, $total;
2710 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2713 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2714 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2718 my $paymenttotal = 0;
2719 foreach my $payment ( $self->_items_payments ) {
2721 $total->{'total_item'} = &$escape_function($payment->{'description'});
2722 $paymenttotal += $payment->{'amount'};
2723 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2724 $adjusttotal += $payment->{'amount'};
2725 if ( $multisection ) {
2726 my $money = $old_latex ? '' : $money_char;
2727 push @detail_items, {
2728 ext_description => [],
2731 description => &$escape_function($payment->{'description'}),
2732 amount => $money. $payment->{'amount'},
2734 section => $adjust_section,
2737 push @total_items, $total;
2739 push @buf, [ $payment->{'description'},
2740 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2743 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2745 if ( $multisection ) {
2746 $adjust_section->{'subtotal'} = $other_money_char.
2747 sprintf('%.2f', $adjusttotal);
2748 push @sections, $adjust_section;
2753 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2754 $total->{'total_amount'} =
2755 &$embolden_function(
2756 $other_money_char. sprintf('%.2f', $summarypage
2758 $self->billing_balance
2759 : $self->owed + $pr_total
2762 if ( $multisection ) {
2763 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2764 $total->{'total_amount'};
2766 push @total_items, $total;
2768 push @buf,['','-----------'];
2769 push @buf,[$self->balance_due_msg, $money_char.
2770 sprintf("%10.2f", $balance_due ) ];
2774 if ( $multisection ) {
2775 push @sections, @$late_sections
2779 my @includelist = ();
2780 push @includelist, 'summary' if $summarypage;
2781 foreach my $include ( @includelist ) {
2783 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2786 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2788 @inc_src = $conf->config($inc_file, $agentnum);
2792 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2794 my $convert_map = $convert_maps{$format}{$include};
2796 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2797 s/--\@\]/$delimiters{$format}[1]/g;
2800 &$convert_map( $conf->config($inc_file, $agentnum) );
2804 my $inc_tt = new Text::Template (
2806 SOURCE => [ map "$_\n", @inc_src ],
2807 DELIMITERS => $delimiters{$format},
2808 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2810 unless ( $inc_tt->compile() ) {
2811 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2812 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2816 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2818 $invoice_data{$include} =~ s/\n+$//
2819 if ($format eq 'latex');
2824 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2825 /invoice_lines\((\d*)\)/;
2826 $invoice_lines += $1 || scalar(@buf);
2829 die "no invoice_lines() functions in template?"
2830 if ( $format eq 'template' && !$wasfunc );
2832 if ($format eq 'template') {
2834 if ( $invoice_lines ) {
2835 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2836 $invoice_data{'total_pages'}++
2837 if scalar(@buf) % $invoice_lines;
2840 #setup subroutine for the template
2841 sub FS::cust_bill::_template::invoice_lines {
2842 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2844 scalar(@FS::cust_bill::_template::buf)
2845 ? shift @FS::cust_bill::_template::buf
2854 push @collect, split("\n",
2855 $text_template->fill_in( HASH => \%invoice_data,
2856 PACKAGE => 'FS::cust_bill::_template'
2859 $FS::cust_bill::_template::page++;
2861 map "$_\n", @collect;
2863 warn "filling in template for invoice ". $self->invnum. "\n"
2865 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2868 $text_template->fill_in(HASH => \%invoice_data);
2872 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2874 Returns an postscript invoice, as a scalar.
2876 Options can be passed as a hashref (recommended) or as a list of time, template
2877 and then any key/value pairs for any other options.
2879 I<time> an optional value used to control the printing of overdue messages. The
2880 default is now. It isn't the date of the invoice; that's the `_date' field.
2881 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2882 L<Time::Local> and L<Date::Parse> for conversion functions.
2884 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2891 my ($file, $lfile) = $self->print_latex(@_);
2892 my $ps = generate_ps($file);
2898 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2900 Returns an PDF invoice, as a scalar.
2902 Options can be passed as a hashref (recommended) or as a list of time, template
2903 and then any key/value pairs for any other options.
2905 I<time> an optional value used to control the printing of overdue messages. The
2906 default is now. It isn't the date of the invoice; that's the `_date' field.
2907 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2908 L<Time::Local> and L<Date::Parse> for conversion functions.
2910 I<template>, if specified, is the name of a suffix for alternate invoices.
2912 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2919 my ($file, $lfile) = $self->print_latex(@_);
2920 my $pdf = generate_pdf($file);
2926 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2928 Returns an HTML invoice, as a scalar.
2930 I<time> an optional value used to control the printing of overdue messages. The
2931 default is now. It isn't the date of the invoice; that's the `_date' field.
2932 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2933 L<Time::Local> and L<Date::Parse> for conversion functions.
2935 I<template>, if specified, is the name of a suffix for alternate invoices.
2937 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2939 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2940 when emailing the invoice as part of a multipart/related MIME email.
2948 %params = %{ shift() };
2950 $params{'time'} = shift;
2951 $params{'template'} = shift;
2952 $params{'cid'} = shift;
2955 $params{'format'} = 'html';
2957 $self->print_generic( %params );
2960 # quick subroutine for print_latex
2962 # There are ten characters that LaTeX treats as special characters, which
2963 # means that they do not simply typeset themselves:
2964 # # $ % & ~ _ ^ \ { }
2966 # TeX ignores blanks following an escaped character; if you want a blank (as
2967 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2971 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2972 $value =~ s/([<>])/\$$1\$/g;
2976 #utility methods for print_*
2978 sub _translate_old_latex_format {
2979 warn "_translate_old_latex_format called\n"
2986 if ( $line =~ /^%%Detail\s*$/ ) {
2988 push @template, q![@--!,
2989 q! foreach my $_tr_line (@detail_items) {!,
2990 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2991 q! $_tr_line->{'description'} .= !,
2992 q! "\\tabularnewline\n~~".!,
2993 q! join( "\\tabularnewline\n~~",!,
2994 q! @{$_tr_line->{'ext_description'}}!,
2998 while ( ( my $line_item_line = shift )
2999 !~ /^%%EndDetail\s*$/ ) {
3000 $line_item_line =~ s/'/\\'/g; # nice LTS
3001 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3002 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3003 push @template, " \$OUT .= '$line_item_line';";
3006 push @template, '}',
3009 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3011 push @template, '[@--',
3012 ' foreach my $_tr_line (@total_items) {';
3014 while ( ( my $total_item_line = shift )
3015 !~ /^%%EndTotalDetails\s*$/ ) {
3016 $total_item_line =~ s/'/\\'/g; # nice LTS
3017 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3018 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3019 push @template, " \$OUT .= '$total_item_line';";
3022 push @template, '}',
3026 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3027 push @template, $line;
3033 warn "$_\n" foreach @template;
3042 #check for an invoice-specific override
3043 return $self->invoice_terms if $self->invoice_terms;
3045 #check for a customer- specific override
3046 my $cust_main = $self->cust_main;
3047 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3049 #use configured default
3050 $conf->config('invoice_default_terms') || '';
3056 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3057 $duedate = $self->_date() + ( $1 * 86400 );
3064 $self->due_date ? time2str(shift, $self->due_date) : '';
3067 sub balance_due_msg {
3069 my $msg = 'Balance Due';
3070 return $msg unless $self->terms;
3071 if ( $self->due_date ) {
3072 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3073 } elsif ( $self->terms ) {
3074 $msg .= ' - '. $self->terms;
3079 sub balance_due_date {
3082 if ( $conf->exists('invoice_default_terms')
3083 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3084 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3089 =item invnum_date_pretty
3091 Returns a string with the invoice number and date, for example:
3092 "Invoice #54 (3/20/2008)"
3096 sub invnum_date_pretty {
3098 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3103 Returns a string with the date, for example: "3/20/2008"
3109 time2str('%x', $self->_date);
3112 use vars qw(%pkg_category_cache);
3113 sub _items_sections {
3116 my $summarypage = shift;
3120 my %late_subtotal = ();
3123 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3126 my $usage = $cust_bill_pkg->usage;
3128 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3129 next if ( $display->summary && $summarypage );
3131 my $section = $display->section;
3132 my $type = $display->type;
3134 $not_tax{$section} = 1
3135 unless $cust_bill_pkg->pkgnum == 0;
3137 if ( $display->post_total && !$summarypage ) {
3138 if (! $type || $type eq 'S') {
3139 $late_subtotal{$section} += $cust_bill_pkg->setup
3140 if $cust_bill_pkg->setup != 0;
3144 $late_subtotal{$section} += $cust_bill_pkg->recur
3145 if $cust_bill_pkg->recur != 0;
3148 if ($type && $type eq 'R') {
3149 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3150 if $cust_bill_pkg->recur != 0;
3153 if ($type && $type eq 'U') {
3154 $late_subtotal{$section} += $usage;
3159 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3161 if (! $type || $type eq 'S') {
3162 $subtotal{$section} += $cust_bill_pkg->setup
3163 if $cust_bill_pkg->setup != 0;
3167 $subtotal{$section} += $cust_bill_pkg->recur
3168 if $cust_bill_pkg->recur != 0;
3171 if ($type && $type eq 'R') {
3172 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3173 if $cust_bill_pkg->recur != 0;
3176 if ($type && $type eq 'U') {
3177 $subtotal{$section} += $usage;
3186 %pkg_category_cache = ();
3188 push @$late, map { { 'description' => &{$escape}($_),
3189 'subtotal' => $late_subtotal{$_},
3192 sort _categorysort keys %late_subtotal;
3195 if ( $summarypage ) {
3196 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3197 map { $_->categoryname } qsearch('pkg_category', {});
3199 @sections = keys %subtotal;
3202 map { { 'description' => &{$escape}($_),
3203 'subtotal' => $subtotal{$_},
3204 'summarized' => $not_tax{$_} ? '' : 'Y',
3205 'tax_section' => $not_tax{$_} ? '' : 'Y',
3208 sort _categorysort @sections;
3212 #helper subs for above
3215 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3219 my $categoryname = shift;
3220 $pkg_category_cache{$categoryname} ||=
3221 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3227 #my @display = scalar(@_)
3229 # : qw( _items_previous _items_pkg );
3230 # #: qw( _items_pkg );
3231 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3232 my @display = qw( _items_previous _items_pkg );
3235 foreach my $display ( @display ) {
3236 push @b, $self->$display(@_);
3241 sub _items_previous {
3243 my $cust_main = $self->cust_main;
3244 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3246 foreach ( @pr_cust_bill ) {
3248 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3249 ' ('. time2str('%x',$_->_date). ')',
3250 #'pkgpart' => 'N/A',
3252 'amount' => sprintf("%.2f", $_->owed),
3258 # 'description' => 'Previous Balance',
3259 # #'pkgpart' => 'N/A',
3260 # 'pkgnum' => 'N/A',
3261 # 'amount' => sprintf("%10.2f", $pr_total ),
3262 # 'ext_description' => [ map {
3263 # "Invoice ". $_->invnum.
3264 # " (". time2str("%x",$_->_date). ") ".
3265 # sprintf("%10.2f", $_->owed)
3266 # } @pr_cust_bill ],
3273 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3274 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3278 return 0 unless $a cmp $b;
3279 return -1 if $b eq 'Tax';
3280 return 1 if $a eq 'Tax';
3281 return -1 if $b eq 'Other surcharges';
3282 return 1 if $a eq 'Other surcharges';
3288 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3289 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3292 sub _items_cust_bill_pkg {
3294 my $cust_bill_pkg = shift;
3297 my $format = $opt{format} || '';
3298 my $escape_function = $opt{escape_function} || sub { shift };
3299 my $format_function = $opt{format_function} || '';
3300 my $unsquelched = $opt{unsquelched} || '';
3301 my $section = $opt{section}->{description} if $opt{section};
3302 my $summary_page = $opt{summary_page} || '';
3305 my ($s, $r, $u) = ( undef, undef, undef );
3306 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3309 foreach ( $s, $r, $u ) {
3310 if ( $_ && !$cust_bill_pkg->hidden ) {
3311 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3312 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3318 foreach my $display ( grep { defined($section)
3319 ? $_->section eq $section
3322 grep { !$_->summary || !$summary_page }
3323 $cust_bill_pkg->cust_bill_pkg_display
3327 my $type = $display->type;
3329 my $desc = $cust_bill_pkg->desc;
3330 $desc = substr($desc, 0, 50). '...'
3331 if $format eq 'latex' && length($desc) > 50;
3333 my %details_opt = ( 'format' => $format,
3334 'escape_function' => $escape_function,
3335 'format_function' => $format_function,
3338 if ( $cust_bill_pkg->pkgnum > 0 ) {
3340 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3342 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3344 my $description = $desc;
3345 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3348 push @d, map &{$escape_function}($_),
3349 $cust_pkg->h_labels_short($self->_date)
3350 unless $cust_pkg->part_pkg->hide_svc_detail
3351 || $cust_bill_pkg->hidden;
3352 push @d, $cust_bill_pkg->details(%details_opt)
3353 if $cust_bill_pkg->recur == 0;
3355 if ( $cust_bill_pkg->hidden ) {
3356 $s->{amount} += $cust_bill_pkg->setup;
3357 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3358 push @{ $s->{ext_description} }, @d;
3361 description => $description,
3362 #pkgpart => $part_pkg->pkgpart,
3363 pkgnum => $cust_bill_pkg->pkgnum,
3364 amount => $cust_bill_pkg->setup,
3365 unit_amount => $cust_bill_pkg->unitsetup,
3366 quantity => $cust_bill_pkg->quantity,
3367 ext_description => \@d,
3373 if ( $cust_bill_pkg->recur != 0 &&
3374 ( !$type || $type eq 'R' || $type eq 'U' )
3378 my $is_summary = $display->summary;
3379 my $description = ($is_summary && $type && $type eq 'U')
3380 ? "Usage charges" : $desc;
3382 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3383 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3384 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3389 #at least until cust_bill_pkg has "past" ranges in addition to
3390 #the "future" sdate/edate ones... see #3032
3391 my @dates = ( $self->_date );
3392 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3393 push @dates, $prev->sdate if $prev;
3395 push @d, map &{$escape_function}($_),
3396 $cust_pkg->h_labels_short(@dates)
3397 #$cust_bill_pkg->edate,
3398 #$cust_bill_pkg->sdate)
3399 unless $cust_pkg->part_pkg->hide_svc_detail
3400 || $cust_bill_pkg->itemdesc
3401 || $cust_bill_pkg->hidden
3402 || $is_summary && $type && $type eq 'U';
3404 push @d, $cust_bill_pkg->details(%details_opt)
3405 unless ($is_summary || $type && $type eq 'R');
3409 $amount = $cust_bill_pkg->recur;
3410 }elsif($type eq 'R') {
3411 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3412 }elsif($type eq 'U') {
3413 $amount = $cust_bill_pkg->usage;
3416 if ( !$type || $type eq 'R' ) {
3418 if ( $cust_bill_pkg->hidden ) {
3419 $r->{amount} += $amount;
3420 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3421 push @{ $r->{ext_description} }, @d;
3424 description => $description,
3425 #pkgpart => $part_pkg->pkgpart,
3426 pkgnum => $cust_bill_pkg->pkgnum,
3428 unit_amount => $cust_bill_pkg->unitrecur,
3429 quantity => $cust_bill_pkg->quantity,
3430 ext_description => \@d,
3434 } elsif ( $amount ) { # && $type eq 'U'
3436 if ( $cust_bill_pkg->hidden ) {
3437 $u->{amount} += $amount;
3438 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3439 push @{ $u->{ext_description} }, @d;
3442 description => $description,
3443 #pkgpart => $part_pkg->pkgpart,
3444 pkgnum => $cust_bill_pkg->pkgnum,
3446 unit_amount => $cust_bill_pkg->unitrecur,
3447 quantity => $cust_bill_pkg->quantity,
3448 ext_description => \@d,
3454 } # recurring or usage with recurring charge
3456 } else { #pkgnum tax or one-shot line item (??)
3458 if ( $cust_bill_pkg->setup != 0 ) {
3460 'description' => $desc,
3461 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3464 if ( $cust_bill_pkg->recur != 0 ) {
3466 'description' => "$desc (".
3467 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3468 time2str("%x", $cust_bill_pkg->edate). ')',
3469 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3479 foreach ( $s, $r, $u ) {
3481 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3482 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3491 sub _items_credits {
3492 my( $self, %opt ) = @_;
3493 my $trim_len = $opt{'trim_len'} || 60;
3497 foreach ( $self->cust_credited ) {
3499 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3501 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3502 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3503 $reason = " ($reason) " if $reason;
3506 #'description' => 'Credit ref\#'. $_->crednum.
3507 # " (". time2str("%x",$_->cust_credit->_date) .")".
3509 'description' => 'Credit applied '.
3510 time2str("%x",$_->cust_credit->_date). $reason,
3511 'amount' => sprintf("%.2f",$_->amount),
3519 sub _items_payments {
3523 #get & print payments
3524 foreach ( $self->cust_bill_pay ) {
3526 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3529 'description' => "Payment received ".
3530 time2str("%x",$_->cust_pay->_date ),
3531 'amount' => sprintf("%.2f", $_->amount )
3539 =item call_details [ OPTION => VALUE ... ]
3541 Returns an array of CSV strings representing the call details for this invoice
3542 The only option available is the boolean prepend_billed_number
3547 my ($self, %opt) = @_;
3549 my $format_function = sub { shift };
3551 if ($opt{prepend_billed_number}) {
3552 $format_function = sub {
3556 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3561 my @details = map { $_->details( 'format_function' => $format_function,
3562 'escape_function' => sub{ return() },
3566 $self->cust_bill_pkg;
3567 my $header = $details[0];
3568 ( $header, grep { $_ ne $header } @details );
3578 =item process_reprint
3582 sub process_reprint {
3583 process_re_X('print', @_);
3586 =item process_reemail
3590 sub process_reemail {
3591 process_re_X('email', @_);
3599 process_re_X('fax', @_);
3607 process_re_X('ftp', @_);
3614 sub process_respool {
3615 process_re_X('spool', @_);
3618 use Storable qw(thaw);
3622 my( $method, $job ) = ( shift, shift );
3623 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3625 my $param = thaw(decode_base64(shift));
3626 warn Dumper($param) if $DEBUG;
3637 my($method, $job, %param ) = @_;
3639 warn "re_X $method for job $job with param:\n".
3640 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3643 #some false laziness w/search/cust_bill.html
3645 my $orderby = 'ORDER BY cust_bill._date';
3647 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3649 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3651 my @cust_bill = qsearch( {
3652 #'select' => "cust_bill.*",
3653 'table' => 'cust_bill',
3654 'addl_from' => $addl_from,
3656 'extra_sql' => $extra_sql,
3657 'order_by' => $orderby,
3661 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3663 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3666 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3667 foreach my $cust_bill ( @cust_bill ) {
3668 $cust_bill->$method();
3670 if ( $job ) { #progressbar foo
3672 if ( time - $min_sec > $last ) {
3673 my $error = $job->update_statustext(
3674 int( 100 * $num / scalar(@cust_bill) )
3676 die $error if $error;
3687 =head1 CLASS METHODS
3693 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3699 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3704 Returns an SQL fragment to retreive the net amount (charged minus credited).
3710 'charged - '. $class->credited_sql;
3715 Returns an SQL fragment to retreive the amount paid against this invoice.
3721 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3722 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3727 Returns an SQL fragment to retreive the amount credited against this invoice.
3733 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3734 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3737 =item search_sql HASHREF
3739 Class method which returns an SQL WHERE fragment to search for parameters
3740 specified in HASHREF. Valid parameters are
3746 List reference of start date, end date, as UNIX timestamps.
3756 List reference of charged limits (exclusive).
3760 List reference of charged limits (exclusive).
3764 flag, return open invoices only
3768 flag, return net invoices only
3772 =item newest_percust
3776 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3781 my($class, $param) = @_;
3783 warn "$me search_sql called with params: \n".
3784 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3790 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3791 push @search, "cust_main.agentnum = $1";
3795 if ( $param->{_date} ) {
3796 my($beginning, $ending) = @{$param->{_date}};
3798 push @search, "cust_bill._date >= $beginning",
3799 "cust_bill._date < $ending";
3803 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3804 push @search, "cust_bill.invnum >= $1";
3806 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3807 push @search, "cust_bill.invnum <= $1";
3811 if ( $param->{charged} ) {
3812 my @charged = ref($param->{charged})
3813 ? @{ $param->{charged} }
3814 : ($param->{charged});
3816 push @search, map { s/^charged/cust_bill.charged/; $_; }
3820 my $owed_sql = FS::cust_bill->owed_sql;
3823 if ( $param->{owed} ) {
3824 my @owed = ref($param->{owed})
3825 ? @{ $param->{owed} }
3827 push @search, map { s/^owed/$owed_sql/; $_; }
3832 push @search, "0 != $owed_sql"
3833 if $param->{'open'};
3834 push @search, '0 != '. FS::cust_bill->net_sql
3838 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3839 if $param->{'days'};
3842 if ( $param->{'newest_percust'} ) {
3844 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3845 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3847 my @newest_where = map { my $x = $_;
3848 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3851 grep ! /^cust_main./, @search;
3852 my $newest_where = scalar(@newest_where)
3853 ? ' AND '. join(' AND ', @newest_where)
3857 push @search, "cust_bill._date = (
3858 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3859 WHERE newest_cust_bill.custnum = cust_bill.custnum
3865 #agent virtualization
3866 my $curuser = $FS::CurrentUser::CurrentUser;
3867 if ( $curuser->username eq 'fs_queue'
3868 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3870 my $newuser = qsearchs('access_user', {
3871 'username' => $username,
3875 $curuser = $newuser;
3877 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3880 push @search, $curuser->agentnums_sql;
3882 join(' AND ', @search );
3894 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3895 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base