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 Storable qw( freeze thaw );
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main_Mixin;
20 use FS::cust_statement;
21 use FS::cust_bill_pkg;
22 use FS::cust_bill_pkg_display;
23 use FS::cust_bill_pkg_detail;
27 use FS::cust_credit_bill;
29 use FS::cust_pay_batch;
30 use FS::cust_bill_event;
33 use FS::cust_bill_pay;
34 use FS::cust_bill_pay_batch;
35 use FS::part_bill_event;
38 @ISA = qw( FS::cust_main_Mixin FS::Record );
41 $me = '[FS::cust_bill]';
43 #ask FS::UID to run this stuff for us later
44 FS::UID->install_callback( sub {
46 $money_char = $conf->config('money_char') || '$';
51 FS::cust_bill - Object methods for cust_bill records
57 $record = new FS::cust_bill \%hash;
58 $record = new FS::cust_bill { 'column' => 'value' };
60 $error = $record->insert;
62 $error = $new_record->replace($old_record);
64 $error = $record->delete;
66 $error = $record->check;
68 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
70 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
72 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
74 @cust_pay_objects = $cust_bill->cust_pay;
76 $tax_amount = $record->tax;
78 @lines = $cust_bill->print_text;
79 @lines = $cust_bill->print_text $time;
83 An FS::cust_bill object represents an invoice; a declaration that a customer
84 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
85 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
86 following fields are currently supported:
92 =item invnum - primary key (assigned automatically for new invoices)
94 =item custnum - customer (see L<FS::cust_main>)
96 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
97 L<Time::Local> and L<Date::Parse> for conversion functions.
99 =item charged - amount of this invoice
101 =item invoice_terms - optional terms override for this specific invoice
105 Customer info at invoice generation time
109 =item previous_balance
111 =item billing_balance
119 =item printed - deprecated
127 =item closed - books closed flag, empty or `Y'
129 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
131 =item agent_invid - legacy invoice number
141 Creates a new invoice. To add the invoice to the database, see L<"insert">.
142 Invoices are normally created by calling the bill method of a customer object
143 (see L<FS::cust_main>).
147 sub table { 'cust_bill'; }
149 sub cust_linked { $_[0]->cust_main_custnum; }
150 sub cust_unlinked_msg {
152 "WARNING: can't find cust_main.custnum ". $self->custnum.
153 ' (cust_bill.invnum '. $self->invnum. ')';
158 Adds this invoice to the database ("Posts" the invoice). If there is an error,
159 returns the error, otherwise returns false.
163 This method now works but you probably shouldn't use it. Instead, apply a
164 credit against the invoice.
166 Using this method to delete invoices outright is really, really bad. There
167 would be no record you ever posted this invoice, and there are no check to
168 make sure charged = 0 or that there are no associated cust_bill_pkg records.
170 Really, don't use it.
176 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
178 local $SIG{HUP} = 'IGNORE';
179 local $SIG{INT} = 'IGNORE';
180 local $SIG{QUIT} = 'IGNORE';
181 local $SIG{TERM} = 'IGNORE';
182 local $SIG{TSTP} = 'IGNORE';
183 local $SIG{PIPE} = 'IGNORE';
185 my $oldAutoCommit = $FS::UID::AutoCommit;
186 local $FS::UID::AutoCommit = 0;
189 foreach my $table (qw(
201 foreach my $linked ( $self->$table() ) {
202 my $error = $linked->delete;
204 $dbh->rollback if $oldAutoCommit;
211 my $error = $self->SUPER::delete(@_);
213 $dbh->rollback if $oldAutoCommit;
217 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
223 =item replace OLD_RECORD
225 Replaces the OLD_RECORD with this one in the database. If there is an error,
226 returns the error, otherwise returns false.
228 Only printed may be changed. printed is normally updated by calling the
229 collect method of a customer object (see L<FS::cust_main>).
233 #replace can be inherited from Record.pm
235 # replace_check is now the preferred way to #implement replace data checks
236 # (so $object->replace() works without an argument)
239 my( $new, $old ) = ( shift, shift );
240 return "Can't change custnum!" unless $old->custnum == $new->custnum;
241 #return "Can't change _date!" unless $old->_date eq $new->_date;
242 return "Can't change _date!" unless $old->_date == $new->_date;
243 return "Can't change charged!" unless $old->charged == $new->charged
244 || $old->charged == 0;
251 Checks all fields to make sure this is a valid invoice. If there is an error,
252 returns the error, otherwise returns false. Called by the insert and replace
261 $self->ut_numbern('invnum')
262 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
263 || $self->ut_numbern('_date')
264 || $self->ut_money('charged')
265 || $self->ut_numbern('printed')
266 || $self->ut_enum('closed', [ '', 'Y' ])
267 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
268 || $self->ut_numbern('agent_invid') #varchar?
270 return $error if $error;
272 $self->_date(time) unless $self->_date;
274 $self->printed(0) if $self->printed eq '';
281 Returns the displayed invoice number for this invoice: agent_invid if
282 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
288 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
289 return $self->agent_invid;
291 return $self->invnum;
297 Returns a list consisting of the total previous balance for this customer,
298 followed by the previous outstanding invoices (as FS::cust_bill objects also).
305 my @cust_bill = sort { $a->_date <=> $b->_date }
306 grep { $_->owed != 0 && $_->_date < $self->_date }
307 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
309 foreach ( @cust_bill ) { $total += $_->owed; }
315 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
322 { 'table' => 'cust_bill_pkg',
323 'hashref' => { 'invnum' => $self->invnum },
324 'order_by' => 'ORDER BY billpkgnum',
329 =item cust_bill_pkg_pkgnum PKGNUM
331 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
336 sub cust_bill_pkg_pkgnum {
337 my( $self, $pkgnum ) = @_;
339 { 'table' => 'cust_bill_pkg',
340 'hashref' => { 'invnum' => $self->invnum,
343 'order_by' => 'ORDER BY billpkgnum',
350 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
357 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
359 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
362 =item open_cust_bill_pkg
364 Returns the open line items for this invoice.
366 Note that cust_bill_pkg with both setup and recur fees are returned as two
367 separate line items, each with only one fee.
371 # modeled after cust_main::open_cust_bill
372 sub open_cust_bill_pkg {
375 # grep { $_->owed > 0 } $self->cust_bill_pkg
377 my %other = ( 'recur' => 'setup',
378 'setup' => 'recur', );
380 foreach my $field ( qw( recur setup )) {
381 push @open, map { $_->set( $other{$field}, 0 ); $_; }
382 grep { $_->owed($field) > 0 }
383 $self->cust_bill_pkg;
389 =item cust_bill_event
391 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
395 sub cust_bill_event {
397 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
400 =item num_cust_bill_event
402 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
406 sub num_cust_bill_event {
409 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
410 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
411 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
412 $sth->fetchrow_arrayref->[0];
417 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
421 #false laziness w/cust_pkg.pm
425 'table' => 'cust_event',
426 'addl_from' => 'JOIN part_event USING ( eventpart )',
427 'hashref' => { 'tablenum' => $self->invnum },
428 'extra_sql' => " AND eventtable = 'cust_bill' ",
434 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
438 #false laziness w/cust_pkg.pm
442 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
443 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
444 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
445 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
446 $sth->fetchrow_arrayref->[0];
451 Returns the customer (see L<FS::cust_main>) for this invoice.
457 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
460 =item cust_suspend_if_balance_over AMOUNT
462 Suspends the customer associated with this invoice if the total amount owed on
463 this invoice and all older invoices is greater than the specified amount.
465 Returns a list: an empty list on success or a list of errors.
469 sub cust_suspend_if_balance_over {
470 my( $self, $amount ) = ( shift, shift );
471 my $cust_main = $self->cust_main;
472 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
475 $cust_main->suspend(@_);
481 Depreciated. See the cust_credited method.
483 #Returns a list consisting of the total previous credited (see
484 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
485 #outstanding credits (FS::cust_credit objects).
491 croak "FS::cust_bill->cust_credit depreciated; see ".
492 "FS::cust_bill->cust_credit_bill";
495 #my @cust_credit = sort { $a->_date <=> $b->_date }
496 # grep { $_->credited != 0 && $_->_date < $self->_date }
497 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
499 #foreach (@cust_credit) { $total += $_->credited; }
500 #$total, @cust_credit;
505 Depreciated. See the cust_bill_pay method.
507 #Returns all payments (see L<FS::cust_pay>) for this invoice.
513 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
515 #sort { $a->_date <=> $b->_date }
516 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
522 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
525 sub cust_bill_pay_batch {
527 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
532 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
538 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
539 sort { $a->_date <=> $b->_date }
540 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
545 =item cust_credit_bill
547 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
553 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
554 sort { $a->_date <=> $b->_date }
555 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
559 sub cust_credit_bill {
560 shift->cust_credited(@_);
563 =item cust_bill_pay_pkgnum PKGNUM
565 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
566 with matching pkgnum.
570 sub cust_bill_pay_pkgnum {
571 my( $self, $pkgnum ) = @_;
572 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
573 sort { $a->_date <=> $b->_date }
574 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
580 =item cust_credited_pkgnum PKGNUM
582 =item cust_credit_bill_pkgnum PKGNUM
584 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
585 with matching pkgnum.
589 sub cust_credited_pkgnum {
590 my( $self, $pkgnum ) = @_;
591 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
592 sort { $a->_date <=> $b->_date }
593 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
599 sub cust_credit_bill_pkgnum {
600 shift->cust_credited_pkgnum(@_);
605 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
612 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
614 foreach (@taxlines) { $total += $_->setup; }
620 Returns the amount owed (still outstanding) on this invoice, which is charged
621 minus all payment applications (see L<FS::cust_bill_pay>) and credit
622 applications (see L<FS::cust_credit_bill>).
628 my $balance = $self->charged;
629 $balance -= $_->amount foreach ( $self->cust_bill_pay );
630 $balance -= $_->amount foreach ( $self->cust_credited );
631 $balance = sprintf( "%.2f", $balance);
632 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
637 my( $self, $pkgnum ) = @_;
639 #my $balance = $self->charged;
641 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
643 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
644 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
646 $balance = sprintf( "%.2f", $balance);
647 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
651 =item apply_payments_and_credits [ OPTION => VALUE ... ]
653 Applies unapplied payments and credits to this invoice.
655 A hash of optional arguments may be passed. Currently "manual" is supported.
656 If true, a payment receipt is sent instead of a statement when
657 'payment_receipt_email' configuration option is set.
659 If there is an error, returns the error, otherwise returns false.
663 sub apply_payments_and_credits {
664 my( $self, %options ) = @_;
666 local $SIG{HUP} = 'IGNORE';
667 local $SIG{INT} = 'IGNORE';
668 local $SIG{QUIT} = 'IGNORE';
669 local $SIG{TERM} = 'IGNORE';
670 local $SIG{TSTP} = 'IGNORE';
671 local $SIG{PIPE} = 'IGNORE';
673 my $oldAutoCommit = $FS::UID::AutoCommit;
674 local $FS::UID::AutoCommit = 0;
677 $self->select_for_update; #mutex
679 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
680 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
682 if ( $conf->exists('pkg-balances') ) {
683 # limit @payments & @credits to those w/ a pkgnum grepped from $self
684 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
685 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
686 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
689 while ( $self->owed > 0 and ( @payments || @credits ) ) {
692 if ( @payments && @credits ) {
694 #decide which goes first by weight of top (unapplied) line item
696 my @open_lineitems = $self->open_cust_bill_pkg;
699 max( map { $_->part_pkg->pay_weight || 0 }
704 my $max_credit_weight =
705 max( map { $_->part_pkg->credit_weight || 0 }
711 #if both are the same... payments first? it has to be something
712 if ( $max_pay_weight >= $max_credit_weight ) {
718 } elsif ( @payments ) {
720 } elsif ( @credits ) {
723 die "guru meditation #12 and 35";
727 if ( $app eq 'pay' ) {
729 my $payment = shift @payments;
730 $unapp_amount = $payment->unapplied;
731 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
732 $app->pkgnum( $payment->pkgnum )
733 if $conf->exists('pkg-balances') && $payment->pkgnum;
735 } elsif ( $app eq 'credit' ) {
737 my $credit = shift @credits;
738 $unapp_amount = $credit->credited;
739 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
740 $app->pkgnum( $credit->pkgnum )
741 if $conf->exists('pkg-balances') && $credit->pkgnum;
744 die "guru meditation #12 and 35";
748 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
749 warn "owed_pkgnum ". $app->pkgnum;
750 $owed = $self->owed_pkgnum($app->pkgnum);
754 next unless $owed > 0;
756 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
757 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
759 $app->invnum( $self->invnum );
761 my $error = $app->insert(%options);
763 $dbh->rollback if $oldAutoCommit;
764 return "Error inserting ". $app->table. " record: $error";
766 die $error if $error;
770 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
775 =item generate_email OPTION => VALUE ...
783 sender address, required
787 alternate template name, optional
791 text attachment arrayref, optional
795 email subject, optional
799 notice name instead of "Invoice", optional
803 Returns an argument list to be passed to L<FS::Misc::send_email>.
814 my $me = '[FS::cust_bill::generate_email]';
817 'from' => $args{'from'},
818 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
822 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
823 'template' => $args{'template'},
824 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
827 my $cust_main = $self->cust_main;
829 if (ref($args{'to'}) eq 'ARRAY') {
830 $return{'to'} = $args{'to'};
832 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
833 $cust_main->invoicing_list
837 if ( $conf->exists('invoice_html') ) {
839 warn "$me creating HTML/text multipart message"
842 $return{'nobody'} = 1;
844 my $alternative = build MIME::Entity
845 'Type' => 'multipart/alternative',
846 'Encoding' => '7bit',
847 'Disposition' => 'inline'
851 if ( $conf->exists('invoice_email_pdf')
852 and scalar($conf->config('invoice_email_pdf_note')) ) {
854 warn "$me using 'invoice_email_pdf_note' in multipart message"
856 $data = [ map { $_ . "\n" }
857 $conf->config('invoice_email_pdf_note')
862 warn "$me not using 'invoice_email_pdf_note' in multipart message"
864 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
865 $data = $args{'print_text'};
867 $data = [ $self->print_text(\%opt) ];
872 $alternative->attach(
873 'Type' => 'text/plain',
874 #'Encoding' => 'quoted-printable',
875 'Encoding' => '7bit',
877 'Disposition' => 'inline',
880 $args{'from'} =~ /\@([\w\.\-]+)/;
881 my $from = $1 || 'example.com';
882 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
885 my $agentnum = $cust_main->agentnum;
886 if ( defined($args{'template'}) && length($args{'template'})
887 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
890 $logo = 'logo_'. $args{'template'}. '.png';
894 my $image_data = $conf->config_binary( $logo, $agentnum);
896 my $image = build MIME::Entity
897 'Type' => 'image/png',
898 'Encoding' => 'base64',
899 'Data' => $image_data,
900 'Filename' => 'logo.png',
901 'Content-ID' => "<$content_id>",
904 $alternative->attach(
905 'Type' => 'text/html',
906 'Encoding' => 'quoted-printable',
907 'Data' => [ '<html>',
910 ' '. encode_entities($return{'subject'}),
913 ' <body bgcolor="#e8e8e8">',
914 $self->print_html({ 'cid'=>$content_id, %opt }),
918 'Disposition' => 'inline',
919 #'Filename' => 'invoice.pdf',
923 if ( $cust_main->email_csv_cdr ) {
925 push @otherparts, build MIME::Entity
926 'Type' => 'text/csv',
927 'Encoding' => '7bit',
928 'Data' => [ map { "$_\n" }
929 $self->call_details('prepend_billed_number' => 1)
931 'Disposition' => 'attachment',
932 'Filename' => 'usage-'. $self->invnum. '.csv',
937 if ( $conf->exists('invoice_email_pdf') ) {
942 # multipart/alternative
948 my $related = build MIME::Entity 'Type' => 'multipart/related',
949 'Encoding' => '7bit';
951 #false laziness w/Misc::send_email
952 $related->head->replace('Content-type',
954 '; boundary="'. $related->head->multipart_boundary. '"'.
955 '; type=multipart/alternative'
958 $related->add_part($alternative);
960 $related->add_part($image);
962 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
964 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
968 #no other attachment:
970 # multipart/alternative
975 $return{'content-type'} = 'multipart/related';
976 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
977 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
978 #$return{'disposition'} = 'inline';
984 if ( $conf->exists('invoice_email_pdf') ) {
985 warn "$me creating PDF attachment"
988 #mime parts arguments a la MIME::Entity->build().
989 $return{'mimeparts'} = [
990 { $self->mimebuild_pdf(\%opt) }
994 if ( $conf->exists('invoice_email_pdf')
995 and scalar($conf->config('invoice_email_pdf_note')) ) {
997 warn "$me using 'invoice_email_pdf_note'"
999 $return{'body'} = [ map { $_ . "\n" }
1000 $conf->config('invoice_email_pdf_note')
1005 warn "$me not using 'invoice_email_pdf_note'"
1007 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1008 $return{'body'} = $args{'print_text'};
1010 $return{'body'} = [ $self->print_text(\%opt) ];
1023 Returns a list suitable for passing to MIME::Entity->build(), representing
1024 this invoice as PDF attachment.
1031 'Type' => 'application/pdf',
1032 'Encoding' => 'base64',
1033 'Data' => [ $self->print_pdf(@_) ],
1034 'Disposition' => 'attachment',
1035 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1039 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1041 Sends this invoice to the destinations configured for this customer: sends
1042 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1044 Options can be passed as a hashref (recommended) or as a list of up to
1045 four values for templatename, agentnum, invoice_from and amount.
1047 I<template>, if specified, is the name of a suffix for alternate invoices.
1049 I<agentnum>, if specified, means that this invoice will only be sent for customers
1050 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1051 single agent) or an arrayref of agentnums.
1053 I<invoice_from>, if specified, overrides the default email invoice From: address.
1055 I<amount>, if specified, only sends the invoice if the total amount owed on this
1056 invoice and all older invoices is greater than the specified amount.
1058 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1062 sub queueable_send {
1065 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1066 or die "invalid invoice number: " . $opt{invnum};
1068 my @args = ( $opt{template}, $opt{agentnum} );
1069 push @args, $opt{invoice_from}
1070 if exists($opt{invoice_from}) && $opt{invoice_from};
1072 my $error = $self->send( @args );
1073 die $error if $error;
1080 my( $template, $invoice_from, $notice_name );
1082 my $balance_over = 0;
1086 $template = $opt->{'template'} || '';
1087 if ( $agentnums = $opt->{'agentnum'} ) {
1088 $agentnums = [ $agentnums ] unless ref($agentnums);
1090 $invoice_from = $opt->{'invoice_from'};
1091 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1092 $notice_name = $opt->{'notice_name'};
1094 $template = scalar(@_) ? shift : '';
1095 if ( scalar(@_) && $_[0] ) {
1096 $agentnums = ref($_[0]) ? shift : [ shift ];
1098 $invoice_from = shift if scalar(@_);
1099 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1102 return 'N/A' unless ! $agentnums
1103 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1106 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1108 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1109 $conf->config('invoice_from', $self->cust_main->agentnum );
1112 'template' => $template,
1113 'invoice_from' => $invoice_from,
1114 'notice_name' => ( $notice_name || 'Invoice' ),
1117 my @invoicing_list = $self->cust_main->invoicing_list;
1119 #$self->email_invoice(\%opt)
1121 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1123 #$self->print_invoice(\%opt)
1125 if grep { $_ eq 'POST' } @invoicing_list; #postal
1127 $self->fax_invoice(\%opt)
1128 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1134 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1136 Emails this invoice.
1138 Options can be passed as a hashref (recommended) or as a list of up to
1139 two values for templatename and invoice_from.
1141 I<template>, if specified, is the name of a suffix for alternate invoices.
1143 I<invoice_from>, if specified, overrides the default email invoice From: address.
1145 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1149 sub queueable_email {
1152 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1153 or die "invalid invoice number: " . $opt{invnum};
1155 my @args = ( $opt{template} );
1156 push @args, $opt{invoice_from}
1157 if exists($opt{invoice_from}) && $opt{invoice_from};
1159 my $error = $self->email( @args );
1160 die $error if $error;
1164 #sub email_invoice {
1168 my( $template, $invoice_from, $notice_name );
1171 $template = $opt->{'template'} || '';
1172 $invoice_from = $opt->{'invoice_from'};
1173 $notice_name = $opt->{'notice_name'} || 'Invoice';
1175 $template = scalar(@_) ? shift : '';
1176 $invoice_from = shift if scalar(@_);
1177 $notice_name = 'Invoice';
1180 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1181 $conf->config('invoice_from', $self->cust_main->agentnum );
1183 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1184 $self->cust_main->invoicing_list;
1186 #better to notify this person than silence
1187 @invoicing_list = ($invoice_from) unless @invoicing_list;
1189 my $subject = $self->email_subject($template);
1191 my $error = send_email(
1192 $self->generate_email(
1193 'from' => $invoice_from,
1194 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1195 'subject' => $subject,
1196 'template' => $template,
1197 'notice_name' => $notice_name,
1200 die "can't email invoice: $error\n" if $error;
1201 #die "$error\n" if $error;
1208 #my $template = scalar(@_) ? shift : '';
1211 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1214 my $cust_main = $self->cust_main;
1215 my $name = $cust_main->name;
1216 my $name_short = $cust_main->name_short;
1217 my $invoice_number = $self->invnum;
1218 my $invoice_date = $self->_date_pretty;
1220 eval qq("$subject");
1223 =item lpr_data HASHREF | [ TEMPLATE ]
1225 Returns the postscript or plaintext for this invoice as an arrayref.
1227 Options can be passed as a hashref (recommended) or as a single optional value
1230 I<template>, if specified, is the name of a suffix for alternate invoices.
1232 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1238 my( $template, $notice_name );
1241 $template = $opt->{'template'} || '';
1242 $notice_name = $opt->{'notice_name'} || 'Invoice';
1244 $template = scalar(@_) ? shift : '';
1245 $notice_name = 'Invoice';
1249 'template' => $template,
1250 'notice_name' => $notice_name,
1253 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1254 [ $self->$method( \%opt ) ];
1257 =item print HASHREF | [ TEMPLATE ]
1259 Prints this invoice.
1261 Options can be passed as a hashref (recommended) or as a single optional
1264 I<template>, if specified, is the name of a suffix for alternate invoices.
1266 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1270 #sub print_invoice {
1273 my( $template, $notice_name );
1276 $template = $opt->{'template'} || '';
1277 $notice_name = $opt->{'notice_name'} || 'Invoice';
1279 $template = scalar(@_) ? shift : '';
1280 $notice_name = 'Invoice';
1284 'template' => $template,
1285 'notice_name' => $notice_name,
1288 do_print $self->lpr_data(\%opt);
1291 =item fax_invoice HASHREF | [ TEMPLATE ]
1295 Options can be passed as a hashref (recommended) or as a single optional
1298 I<template>, if specified, is the name of a suffix for alternate invoices.
1300 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1306 my( $template, $notice_name );
1309 $template = $opt->{'template'} || '';
1310 $notice_name = $opt->{'notice_name'} || 'Invoice';
1312 $template = scalar(@_) ? shift : '';
1313 $notice_name = 'Invoice';
1316 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1317 unless $conf->exists('invoice_latex');
1319 my $dialstring = $self->cust_main->getfield('fax');
1323 'template' => $template,
1324 'notice_name' => $notice_name,
1327 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1328 'dialstring' => $dialstring,
1330 die $error if $error;
1334 =item ftp_invoice [ TEMPLATENAME ]
1336 Sends this invoice data via FTP.
1338 TEMPLATENAME is unused?
1344 my $template = scalar(@_) ? shift : '';
1347 'protocol' => 'ftp',
1348 'server' => $conf->config('cust_bill-ftpserver'),
1349 'username' => $conf->config('cust_bill-ftpusername'),
1350 'password' => $conf->config('cust_bill-ftppassword'),
1351 'dir' => $conf->config('cust_bill-ftpdir'),
1352 'format' => $conf->config('cust_bill-ftpformat'),
1356 =item spool_invoice [ TEMPLATENAME ]
1358 Spools this invoice data (see L<FS::spool_csv>)
1360 TEMPLATENAME is unused?
1366 my $template = scalar(@_) ? shift : '';
1369 'format' => $conf->config('cust_bill-spoolformat'),
1370 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1374 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1376 Like B<send>, but only sends the invoice if it is the newest open invoice for
1381 sub send_if_newest {
1386 grep { $_->owed > 0 }
1387 qsearch('cust_bill', {
1388 'custnum' => $self->custnum,
1389 #'_date' => { op=>'>', value=>$self->_date },
1390 'invnum' => { op=>'>', value=>$self->invnum },
1397 =item send_csv OPTION => VALUE, ...
1399 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1403 protocol - currently only "ftp"
1409 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1410 and YYMMDDHHMMSS is a timestamp.
1412 See L</print_csv> for a description of the output format.
1417 my($self, %opt) = @_;
1421 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1422 mkdir $spooldir, 0700 unless -d $spooldir;
1424 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1425 my $file = "$spooldir/$tracctnum.csv";
1427 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1429 open(CSV, ">$file") or die "can't open $file: $!";
1437 if ( $opt{protocol} eq 'ftp' ) {
1438 eval "use Net::FTP;";
1440 $net = Net::FTP->new($opt{server}) or die @$;
1442 die "unknown protocol: $opt{protocol}";
1445 $net->login( $opt{username}, $opt{password} )
1446 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1448 $net->binary or die "can't set binary mode";
1450 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1452 $net->put($file) or die "can't put $file: $!";
1462 Spools CSV invoice data.
1468 =item format - 'default' or 'billco'
1470 =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>).
1472 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1474 =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.
1481 my($self, %opt) = @_;
1483 my $cust_main = $self->cust_main;
1485 if ( $opt{'dest'} ) {
1486 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1487 $cust_main->invoicing_list;
1488 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1489 || ! keys %invoicing_list;
1492 if ( $opt{'balanceover'} ) {
1494 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1497 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1498 mkdir $spooldir, 0700 unless -d $spooldir;
1500 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1504 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1505 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1508 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1510 open(CSV, ">>$file") or die "can't open $file: $!";
1511 flock(CSV, LOCK_EX);
1516 if ( lc($opt{'format'}) eq 'billco' ) {
1518 flock(CSV, LOCK_UN);
1523 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1526 open(CSV,">>$file") or die "can't open $file: $!";
1527 flock(CSV, LOCK_EX);
1533 flock(CSV, LOCK_UN);
1540 =item print_csv OPTION => VALUE, ...
1542 Returns CSV data for this invoice.
1546 format - 'default' or 'billco'
1548 Returns a list consisting of two scalars. The first is a single line of CSV
1549 header information for this invoice. The second is one or more lines of CSV
1550 detail information for this invoice.
1552 If I<format> is not specified or "default", the fields of the CSV file are as
1555 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1559 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1561 B<record_type> is C<cust_bill> for the initial header line only. The
1562 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1563 fields are filled in.
1565 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1566 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1569 =item invnum - invoice number
1571 =item custnum - customer number
1573 =item _date - invoice date
1575 =item charged - total invoice amount
1577 =item first - customer first name
1579 =item last - customer first name
1581 =item company - company name
1583 =item address1 - address line 1
1585 =item address2 - address line 1
1595 =item pkg - line item description
1597 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1599 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1601 =item sdate - start date for recurring fee
1603 =item edate - end date for recurring fee
1607 If I<format> is "billco", the fields of the header CSV file are as follows:
1609 +-------------------------------------------------------------------+
1610 | FORMAT HEADER FILE |
1611 |-------------------------------------------------------------------|
1612 | Field | Description | Name | Type | Width |
1613 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1614 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1615 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1616 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1617 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1618 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1619 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1620 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1621 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1622 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1623 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1624 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1625 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1626 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1627 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1628 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1629 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1630 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1631 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1632 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1633 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1634 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1635 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1636 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1637 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1638 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1639 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1640 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1641 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1642 +-------+-------------------------------+------------+------+-------+
1644 If I<format> is "billco", the fields of the detail CSV file are as follows:
1646 FORMAT FOR DETAIL FILE
1648 Field | Description | Name | Type | Width
1649 1 | N/A-Leave Empty | RC | CHAR | 2
1650 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1651 3 | Account Number | TRACCTNUM | CHAR | 15
1652 4 | Invoice Number | TRINVOICE | CHAR | 15
1653 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1654 6 | Transaction Detail | DETAILS | CHAR | 100
1655 7 | Amount | AMT | NUM* | 9
1656 8 | Line Format Control** | LNCTRL | CHAR | 2
1657 9 | Grouping Code | GROUP | CHAR | 2
1658 10 | User Defined | ACCT CODE | CHAR | 15
1663 my($self, %opt) = @_;
1665 eval "use Text::CSV_XS";
1668 my $cust_main = $self->cust_main;
1670 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1672 if ( lc($opt{'format'}) eq 'billco' ) {
1675 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1677 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1679 my( $previous_balance, @unused ) = $self->previous; #previous balance
1681 my $pmt_cr_applied = 0;
1682 $pmt_cr_applied += $_->{'amount'}
1683 foreach ( $self->_items_payments, $self->_items_credits ) ;
1685 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1688 '', # 1 | N/A-Leave Empty CHAR 2
1689 '', # 2 | N/A-Leave Empty CHAR 15
1690 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1691 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1692 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1693 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1694 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1695 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1696 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1697 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1698 '', # 10 | Ancillary Billing Information CHAR 30
1699 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1700 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1703 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1706 $duedate, # 14 | Bill Due Date CHAR 10
1708 $previous_balance, # 15 | Previous Balance NUM* 9
1709 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1710 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1711 $totaldue, # 18 | Total Amt Due NUM* 9
1712 $totaldue, # 19 | Total Amt Due NUM* 9
1713 '', # 20 | 30 Day Aging NUM* 9
1714 '', # 21 | 60 Day Aging NUM* 9
1715 '', # 22 | 90 Day Aging NUM* 9
1716 'N', # 23 | Y/N CHAR 1
1717 '', # 24 | Remittance automation CHAR 100
1718 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1719 $self->custnum, # 26 | Customer Reference Number CHAR 15
1720 '0', # 27 | Federal Tax*** NUM* 9
1721 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1722 '0', # 29 | Other Taxes & Fees*** NUM* 9
1731 time2str("%x", $self->_date),
1732 sprintf("%.2f", $self->charged),
1733 ( map { $cust_main->getfield($_) }
1734 qw( first last company address1 address2 city state zip country ) ),
1736 ) or die "can't create csv";
1739 my $header = $csv->string. "\n";
1742 if ( lc($opt{'format'}) eq 'billco' ) {
1745 foreach my $item ( $self->_items_pkg ) {
1748 '', # 1 | N/A-Leave Empty CHAR 2
1749 '', # 2 | N/A-Leave Empty CHAR 15
1750 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1751 $self->invnum, # 4 | Invoice Number CHAR 15
1752 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1753 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1754 $item->{'amount'}, # 7 | Amount NUM* 9
1755 '', # 8 | Line Format Control** CHAR 2
1756 '', # 9 | Grouping Code CHAR 2
1757 '', # 10 | User Defined CHAR 15
1760 $detail .= $csv->string. "\n";
1766 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1768 my($pkg, $setup, $recur, $sdate, $edate);
1769 if ( $cust_bill_pkg->pkgnum ) {
1771 ($pkg, $setup, $recur, $sdate, $edate) = (
1772 $cust_bill_pkg->part_pkg->pkg,
1773 ( $cust_bill_pkg->setup != 0
1774 ? sprintf("%.2f", $cust_bill_pkg->setup )
1776 ( $cust_bill_pkg->recur != 0
1777 ? sprintf("%.2f", $cust_bill_pkg->recur )
1779 ( $cust_bill_pkg->sdate
1780 ? time2str("%x", $cust_bill_pkg->sdate)
1782 ($cust_bill_pkg->edate
1783 ?time2str("%x", $cust_bill_pkg->edate)
1787 } else { #pkgnum tax
1788 next unless $cust_bill_pkg->setup != 0;
1789 $pkg = $cust_bill_pkg->desc;
1790 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1791 ( $sdate, $edate ) = ( '', '' );
1797 ( map { '' } (1..11) ),
1798 ($pkg, $setup, $recur, $sdate, $edate)
1799 ) or die "can't create csv";
1801 $detail .= $csv->string. "\n";
1807 ( $header, $detail );
1813 Pays this invoice with a compliemntary payment. If there is an error,
1814 returns the error, otherwise returns false.
1820 my $cust_pay = new FS::cust_pay ( {
1821 'invnum' => $self->invnum,
1822 'paid' => $self->owed,
1825 'payinfo' => $self->cust_main->payinfo,
1833 Attempts to pay this invoice with a credit card payment via a
1834 Business::OnlinePayment realtime gateway. See
1835 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1836 for supported processors.
1842 $self->realtime_bop( 'CC', @_ );
1847 Attempts to pay this invoice with an electronic check (ACH) payment via a
1848 Business::OnlinePayment realtime gateway. See
1849 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1850 for supported processors.
1856 $self->realtime_bop( 'ECHECK', @_ );
1861 Attempts to pay this invoice with phone bill (LEC) payment via a
1862 Business::OnlinePayment realtime gateway. See
1863 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1864 for supported processors.
1870 $self->realtime_bop( 'LEC', @_ );
1874 my( $self, $method ) = @_;
1876 my $cust_main = $self->cust_main;
1877 my $balance = $cust_main->balance;
1878 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1879 $amount = sprintf("%.2f", $amount);
1880 return "not run (balance $balance)" unless $amount > 0;
1882 my $description = 'Internet Services';
1883 if ( $conf->exists('business-onlinepayment-description') ) {
1884 my $dtempl = $conf->config('business-onlinepayment-description');
1886 my $agent_obj = $cust_main->agent
1887 or die "can't retreive agent for $cust_main (agentnum ".
1888 $cust_main->agentnum. ")";
1889 my $agent = $agent_obj->agent;
1890 my $pkgs = join(', ',
1891 map { $_->part_pkg->pkg }
1892 grep { $_->pkgnum } $self->cust_bill_pkg
1894 $description = eval qq("$dtempl");
1897 $cust_main->realtime_bop($method, $amount,
1898 'description' => $description,
1899 'invnum' => $self->invnum,
1904 =item batch_card OPTION => VALUE...
1906 Adds a payment for this invoice to the pending credit card batch (see
1907 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1908 runs the payment using a realtime gateway.
1913 my ($self, %options) = @_;
1914 my $cust_main = $self->cust_main;
1916 $options{invnum} = $self->invnum;
1918 $cust_main->batch_card(%options);
1921 sub _agent_template {
1923 $self->cust_main->agent_template;
1926 sub _agent_invoice_from {
1928 $self->cust_main->agent_invoice_from;
1931 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1933 Returns an text invoice, as a list of lines.
1935 Options can be passed as a hashref (recommended) or as a list of time, template
1936 and then any key/value pairs for any other options.
1938 I<time>, if specified, is used to control the printing of overdue messages. The
1939 default is now. It isn't the date of the invoice; that's the `_date' field.
1940 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1941 L<Time::Local> and L<Date::Parse> for conversion functions.
1943 I<template>, if specified, is the name of a suffix for alternate invoices.
1945 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1951 my( $today, $template, %opt );
1953 %opt = %{ shift() };
1954 $today = delete($opt{'time'}) || '';
1955 $template = delete($opt{template}) || '';
1957 ( $today, $template, %opt ) = @_;
1960 my %params = ( 'format' => 'template' );
1961 $params{'time'} = $today if $today;
1962 $params{'template'} = $template if $template;
1963 $params{$_} = $opt{$_}
1964 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1966 $self->print_generic( %params );
1969 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1971 Internal method - returns a filename of a filled-in LaTeX template for this
1972 invoice (Note: add ".tex" to get the actual filename), and a filename of
1973 an associated logo (with the .eps extension included).
1975 See print_ps and print_pdf for methods that return PostScript and PDF output.
1977 Options can be passed as a hashref (recommended) or as a list of time, template
1978 and then any key/value pairs for any other options.
1980 I<time>, if specified, is used to control the printing of overdue messages. The
1981 default is now. It isn't the date of the invoice; that's the `_date' field.
1982 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1983 L<Time::Local> and L<Date::Parse> for conversion functions.
1985 I<template>, if specified, is the name of a suffix for alternate invoices.
1987 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1993 my( $today, $template, %opt );
1995 %opt = %{ shift() };
1996 $today = delete($opt{'time'}) || '';
1997 $template = delete($opt{template}) || '';
1999 ( $today, $template, %opt ) = @_;
2002 my %params = ( 'format' => 'latex' );
2003 $params{'time'} = $today if $today;
2004 $params{'template'} = $template if $template;
2005 $params{$_} = $opt{$_}
2006 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2008 $template ||= $self->_agent_template;
2010 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2011 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2015 ) or die "can't open temp file: $!\n";
2017 my $agentnum = $self->cust_main->agentnum;
2019 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2020 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2021 or die "can't write temp file: $!\n";
2023 print $lh $conf->config_binary('logo.eps', $agentnum)
2024 or die "can't write temp file: $!\n";
2027 $params{'logo_file'} = $lh->filename;
2029 my @filled_in = $self->print_generic( %params );
2031 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2035 ) or die "can't open temp file: $!\n";
2036 print $fh join('', @filled_in );
2039 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2040 return ($1, $params{'logo_file'});
2044 =item print_generic OPTION => VALUE ...
2046 Internal method - returns a filled-in template for this invoice as a scalar.
2048 See print_ps and print_pdf for methods that return PostScript and PDF output.
2050 Non optional options include
2051 format - latex, html, template
2053 Optional options include
2055 template - a value used as a suffix for a configuration template
2057 time - a value used to control the printing of overdue messages. The
2058 default is now. It isn't the date of the invoice; that's the `_date' field.
2059 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2060 L<Time::Local> and L<Date::Parse> for conversion functions.
2064 unsquelch_cdr - overrides any per customer cdr squelching when true
2066 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2070 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2071 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2072 # yes: fixed width (dot matrix) text printing will be borked
2075 my( $self, %params ) = @_;
2076 my $today = $params{today} ? $params{today} : time;
2077 warn "$me print_generic called on $self with suffix $params{template}\n"
2080 my $format = $params{format};
2081 die "Unknown format: $format"
2082 unless $format =~ /^(latex|html|template)$/;
2084 my $cust_main = $self->cust_main;
2085 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2086 unless $cust_main->payname
2087 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2089 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2090 'html' => [ '<%=', '%>' ],
2091 'template' => [ '{', '}' ],
2094 #create the template
2095 my $template = $params{template} ? $params{template} : $self->_agent_template;
2096 my $templatefile = "invoice_$format";
2097 $templatefile .= "_$template"
2098 if length($template);
2099 my @invoice_template = map "$_\n", $conf->config($templatefile)
2100 or die "cannot load config data $templatefile";
2103 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2104 #change this to a die when the old code is removed
2105 warn "old-style invoice template $templatefile; ".
2106 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2107 $old_latex = 'true';
2108 @invoice_template = _translate_old_latex_format(@invoice_template);
2111 my $text_template = new Text::Template(
2113 SOURCE => \@invoice_template,
2114 DELIMITERS => $delimiters{$format},
2117 $text_template->compile()
2118 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2121 # additional substitution could possibly cause breakage in existing templates
2122 my %convert_maps = (
2124 'notes' => sub { map "$_", @_ },
2125 'footer' => sub { map "$_", @_ },
2126 'smallfooter' => sub { map "$_", @_ },
2127 'returnaddress' => sub { map "$_", @_ },
2128 'coupon' => sub { map "$_", @_ },
2129 'summary' => sub { map "$_", @_ },
2135 s/%%(.*)$/<!-- $1 -->/g;
2136 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2137 s/\\begin\{enumerate\}/<ol>/g;
2139 s/\\end\{enumerate\}/<\/ol>/g;
2140 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2149 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2151 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2156 s/\\\\\*?\s*$/<BR>/;
2157 s/\\hyphenation\{[\w\s\-]+}//;
2162 'coupon' => sub { "" },
2163 'summary' => sub { "" },
2170 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2171 s/\\begin\{enumerate\}//g;
2173 s/\\end\{enumerate\}//g;
2174 s/\\textbf\{(.*)\}/$1/g;
2181 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2183 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2188 s/\\\\\*?\s*$/\n/; # dubious
2189 s/\\hyphenation\{[\w\s\-]+}//;
2193 'coupon' => sub { "" },
2194 'summary' => sub { "" },
2199 # hashes for differing output formats
2200 my %nbsps = ( 'latex' => '~',
2201 'html' => '', # '&nbps;' would be nice
2202 'template' => '', # not used
2204 my $nbsp = $nbsps{$format};
2206 my %escape_functions = ( 'latex' => \&_latex_escape,
2207 'html' => \&encode_entities,
2208 'template' => sub { shift },
2210 my $escape_function = $escape_functions{$format};
2212 my %date_formats = ( 'latex' => '%b %o, %Y',
2213 'html' => '%b %o, %Y',
2216 my $date_format = $date_formats{$format};
2218 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2220 'html' => sub { return '<b>'. shift(). '</b>'
2222 'template' => sub { shift },
2224 my $embolden_function = $embolden_functions{$format};
2227 # generate template variables
2230 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2234 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2240 $returnaddress = join("\n",
2241 $conf->config_orbase("invoice_${format}returnaddress", $template)
2244 } elsif ( grep /\S/,
2245 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2247 my $convert_map = $convert_maps{$format}{'returnaddress'};
2250 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2255 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2257 my $convert_map = $convert_maps{$format}{'returnaddress'};
2258 $returnaddress = join( "\n", &$convert_map(
2259 map { s/( {2,})/'~' x length($1)/eg;
2263 ( $conf->config('company_name', $self->cust_main->agentnum),
2264 $conf->config('company_address', $self->cust_main->agentnum),
2271 my $warning = "Couldn't find a return address; ".
2272 "do you need to set the company_address configuration value?";
2274 $returnaddress = $nbsp;
2275 #$returnaddress = $warning;
2279 my %invoice_data = (
2282 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2283 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2284 'returnaddress' => $returnaddress,
2285 'agent' => &$escape_function($cust_main->agent->agent),
2288 'invnum' => $self->invnum,
2289 'date' => time2str($date_format, $self->_date),
2290 'today' => time2str('%b %o, %Y', $today),
2291 'terms' => $self->terms,
2292 'template' => $template, #params{'template'},
2293 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2294 'current_charges' => sprintf("%.2f", $self->charged),
2295 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2298 'custnum' => $cust_main->display_custnum,
2299 'agent_custid' => &$escape_function($cust_main->agent_custid),
2300 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2301 payname company address1 address2 city state zip fax
2305 'ship_enable' => $conf->exists('invoice-ship_address'),
2306 'unitprices' => $conf->exists('invoice-unitprice'),
2307 'smallernotes' => $conf->exists('invoice-smallernotes'),
2308 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2310 # better hang on to conf_dir for a while (for old templates)
2311 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2313 #these are only used when doing paged plaintext
2319 $invoice_data{finance_section} = '';
2320 if ( $conf->config('finance_pkgclass') ) {
2322 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2323 $invoice_data{finance_section} = $pkg_class->categoryname;
2325 $invoice_data{finance_amount} = '0.00';
2327 my $countrydefault = $conf->config('countrydefault') || 'US';
2328 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2329 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2330 my $method = $prefix.$_;
2331 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2333 $invoice_data{'ship_country'} = ''
2334 if ( $invoice_data{'ship_country'} eq $countrydefault );
2336 $invoice_data{'cid'} = $params{'cid'}
2339 if ( $cust_main->country eq $countrydefault ) {
2340 $invoice_data{'country'} = '';
2342 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2346 $invoice_data{'address'} = \@address;
2348 $cust_main->payname.
2349 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2350 ? " (P.O. #". $cust_main->payinfo. ")"
2354 push @address, $cust_main->company
2355 if $cust_main->company;
2356 push @address, $cust_main->address1;
2357 push @address, $cust_main->address2
2358 if $cust_main->address2;
2360 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2361 push @address, $invoice_data{'country'}
2362 if $invoice_data{'country'};
2364 while (scalar(@address) < 5);
2366 $invoice_data{'logo_file'} = $params{'logo_file'}
2367 if $params{'logo_file'};
2369 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2370 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2371 #my $balance_due = $self->owed + $pr_total - $cr_total;
2372 my $balance_due = $self->owed + $pr_total;
2373 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2374 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2375 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2376 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2378 my $agentnum = $self->cust_main->agentnum;
2380 my $summarypage = '';
2381 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2384 $invoice_data{'summarypage'} = $summarypage;
2386 #do variable substitution in notes, footer, smallfooter
2387 foreach my $include (qw( notes footer smallfooter coupon )) {
2389 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2392 if ( $conf->exists($inc_file, $agentnum)
2393 && length( $conf->config($inc_file, $agentnum) ) ) {
2395 @inc_src = $conf->config($inc_file, $agentnum);
2399 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2401 my $convert_map = $convert_maps{$format}{$include};
2403 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2404 s/--\@\]/$delimiters{$format}[1]/g;
2407 &$convert_map( $conf->config($inc_file, $agentnum) );
2411 my $inc_tt = new Text::Template (
2413 SOURCE => [ map "$_\n", @inc_src ],
2414 DELIMITERS => $delimiters{$format},
2415 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2417 unless ( $inc_tt->compile() ) {
2418 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2419 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2423 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2425 $invoice_data{$include} =~ s/\n+$//
2426 if ($format eq 'latex');
2429 $invoice_data{'po_line'} =
2430 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2431 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2434 my %money_chars = ( 'latex' => '',
2435 'html' => $conf->config('money_char') || '$',
2438 my $money_char = $money_chars{$format};
2440 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2441 'html' => $conf->config('money_char') || '$',
2444 my $other_money_char = $other_money_chars{$format};
2445 $invoice_data{'dollar'} = $other_money_char;
2447 my @detail_items = ();
2448 my @total_items = ();
2452 $invoice_data{'detail_items'} = \@detail_items;
2453 $invoice_data{'total_items'} = \@total_items;
2454 $invoice_data{'buf'} = \@buf;
2455 $invoice_data{'sections'} = \@sections;
2457 my $previous_section = { 'description' => 'Previous Charges',
2458 'subtotal' => $other_money_char.
2459 sprintf('%.2f', $pr_total),
2460 'summarized' => $summarypage ? 'Y' : '',
2464 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2465 'subtotal' => $taxtotal, # adjusted below
2466 'summarized' => $summarypage ? 'Y' : '',
2468 my $tax_weight = _pkg_category($tax_section->{description})
2469 ? _pkg_category($tax_section->{description})->weight
2471 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2472 $tax_section->{'sort_weight'} = $tax_weight;
2475 my $adjusttotal = 0;
2476 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2477 'subtotal' => 0, # adjusted below
2478 'summarized' => $summarypage ? 'Y' : '',
2480 my $adjust_weight = _pkg_category($adjust_section->{description})
2481 ? _pkg_category($adjust_section->{description})->weight
2483 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2484 $adjust_section->{'sort_weight'} = $adjust_weight;
2486 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2487 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2488 my $late_sections = [];
2489 my $extra_sections = ();
2490 my $extra_lines = ();
2491 if ( $multisection ) {
2492 ($extra_sections, $extra_lines) =
2493 $self->_items_extra_usage_sections($escape_function, $format)
2494 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2496 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2498 push @detail_items, @$extra_lines if $extra_lines;
2500 $self->_items_sections( $late_sections, # this could stand a refactor
2506 if ($conf->exists('svc_phone_sections')) {
2507 my ($phone_sections, $phone_lines) =
2508 $self->_items_svc_phone_sections($escape_function, $format);
2509 push @{$late_sections}, @$phone_sections;
2510 push @detail_items, @$phone_lines;
2513 push @sections, { 'description' => '', 'subtotal' => '' };
2516 unless ( $conf->exists('disable_previous_balance')
2517 || $conf->exists('previous_balance-summary_only')
2521 foreach my $line_item ( $self->_items_previous ) {
2524 ext_description => [],
2526 $detail->{'ref'} = $line_item->{'pkgnum'};
2527 $detail->{'quantity'} = 1;
2528 $detail->{'section'} = $previous_section;
2529 $detail->{'description'} = &$escape_function($line_item->{'description'});
2530 if ( exists $line_item->{'ext_description'} ) {
2531 @{$detail->{'ext_description'}} = map {
2532 &$escape_function($_);
2533 } @{$line_item->{'ext_description'}};
2535 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2536 $line_item->{'amount'};
2537 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2539 push @detail_items, $detail;
2540 push @buf, [ $detail->{'description'},
2541 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2547 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2548 push @buf, ['','-----------'];
2549 push @buf, [ 'Total Previous Balance',
2550 $money_char. sprintf("%10.2f", $pr_total) ];
2554 foreach my $section (@sections, @$late_sections) {
2556 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2557 if ( $invoice_data{finance_section} &&
2558 $section->{'description'} eq $invoice_data{finance_section} );
2560 $section->{'subtotal'} = $other_money_char.
2561 sprintf('%.2f', $section->{'subtotal'})
2564 # begin some normalization
2565 $section->{'amount'} = $section->{'subtotal'}
2569 if ( $section->{'description'} ) {
2570 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2576 $options{'section'} = $section if $multisection;
2577 $options{'format'} = $format;
2578 $options{'escape_function'} = $escape_function;
2579 $options{'format_function'} = sub { () } unless $unsquelched;
2580 $options{'unsquelched'} = $unsquelched;
2581 $options{'summary_page'} = $summarypage;
2582 $options{'skip_usage'} =
2583 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2585 foreach my $line_item ( $self->_items_pkg(%options) ) {
2587 ext_description => [],
2589 $detail->{'ref'} = $line_item->{'pkgnum'};
2590 $detail->{'quantity'} = $line_item->{'quantity'};
2591 $detail->{'section'} = $section;
2592 $detail->{'description'} = &$escape_function($line_item->{'description'});
2593 if ( exists $line_item->{'ext_description'} ) {
2594 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2596 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2597 $line_item->{'amount'};
2598 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2599 $line_item->{'unit_amount'};
2600 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2602 push @detail_items, $detail;
2603 push @buf, ( [ $detail->{'description'},
2604 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2606 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2610 if ( $section->{'description'} ) {
2611 push @buf, ( ['','-----------'],
2612 [ $section->{'description'}. ' sub-total',
2613 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2622 $invoice_data{current_less_finance} =
2623 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2625 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2626 unshift @sections, $previous_section if $pr_total;
2629 foreach my $tax ( $self->_items_tax ) {
2631 $taxtotal += $tax->{'amount'};
2633 my $description = &$escape_function( $tax->{'description'} );
2634 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2636 if ( $multisection ) {
2638 my $money = $old_latex ? '' : $money_char;
2639 push @detail_items, {
2640 ext_description => [],
2643 description => $description,
2644 amount => $money. $amount,
2646 section => $tax_section,
2651 push @total_items, {
2652 'total_item' => $description,
2653 'total_amount' => $other_money_char. $amount,
2658 push @buf,[ $description,
2659 $money_char. $amount,
2666 $total->{'total_item'} = 'Sub-total';
2667 $total->{'total_amount'} =
2668 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2670 if ( $multisection ) {
2671 $tax_section->{'subtotal'} = $other_money_char.
2672 sprintf('%.2f', $taxtotal);
2673 $tax_section->{'pretotal'} = 'New charges sub-total '.
2674 $total->{'total_amount'};
2675 push @sections, $tax_section if $taxtotal;
2677 unshift @total_items, $total;
2680 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2682 push @buf,['','-----------'];
2683 push @buf,[( $conf->exists('disable_previous_balance')
2685 : 'Total New Charges'
2687 $money_char. sprintf("%10.2f",$self->charged) ];
2692 $total->{'total_item'} = &$embolden_function('Total');
2693 $total->{'total_amount'} =
2694 &$embolden_function(
2697 $self->charged + ( $conf->exists('disable_previous_balance')
2703 if ( $multisection ) {
2704 if ( $adjust_section->{'sort_weight'} ) {
2705 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2706 sprintf("%.2f", ($self->billing_balance || 0) );
2708 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2709 sprintf('%.2f', $self->charged );
2712 push @total_items, $total;
2714 push @buf,['','-----------'];
2715 push @buf,['Total Charges',
2717 sprintf( '%10.2f', $self->charged +
2718 ( $conf->exists('disable_previous_balance')
2727 unless ( $conf->exists('disable_previous_balance') ) {
2728 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2731 my $credittotal = 0;
2732 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2735 $total->{'total_item'} = &$escape_function($credit->{'description'});
2736 $credittotal += $credit->{'amount'};
2737 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2738 $adjusttotal += $credit->{'amount'};
2739 if ( $multisection ) {
2740 my $money = $old_latex ? '' : $money_char;
2741 push @detail_items, {
2742 ext_description => [],
2745 description => &$escape_function($credit->{'description'}),
2746 amount => $money. $credit->{'amount'},
2748 section => $adjust_section,
2751 push @total_items, $total;
2755 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2758 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2759 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2763 my $paymenttotal = 0;
2764 foreach my $payment ( $self->_items_payments ) {
2766 $total->{'total_item'} = &$escape_function($payment->{'description'});
2767 $paymenttotal += $payment->{'amount'};
2768 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2769 $adjusttotal += $payment->{'amount'};
2770 if ( $multisection ) {
2771 my $money = $old_latex ? '' : $money_char;
2772 push @detail_items, {
2773 ext_description => [],
2776 description => &$escape_function($payment->{'description'}),
2777 amount => $money. $payment->{'amount'},
2779 section => $adjust_section,
2782 push @total_items, $total;
2784 push @buf, [ $payment->{'description'},
2785 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2788 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2790 if ( $multisection ) {
2791 $adjust_section->{'subtotal'} = $other_money_char.
2792 sprintf('%.2f', $adjusttotal);
2793 push @sections, $adjust_section
2794 unless $adjust_section->{sort_weight};
2799 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2800 $total->{'total_amount'} =
2801 &$embolden_function(
2802 $other_money_char. sprintf('%.2f', $summarypage
2804 $self->billing_balance
2805 : $self->owed + $pr_total
2808 if ( $multisection && !$adjust_section->{sort_weight} ) {
2809 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2810 $total->{'total_amount'};
2812 push @total_items, $total;
2814 push @buf,['','-----------'];
2815 push @buf,[$self->balance_due_msg, $money_char.
2816 sprintf("%10.2f", $balance_due ) ];
2820 if ( $multisection ) {
2821 if ($conf->exists('svc_phone_sections')) {
2823 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2824 $total->{'total_amount'} =
2825 &$embolden_function(
2826 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2828 my $last_section = pop @sections;
2829 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2830 $total->{'total_amount'};
2831 push @sections, $last_section;
2833 push @sections, @$late_sections
2837 my @includelist = ();
2838 push @includelist, 'summary' if $summarypage;
2839 foreach my $include ( @includelist ) {
2841 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2844 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2846 @inc_src = $conf->config($inc_file, $agentnum);
2850 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2852 my $convert_map = $convert_maps{$format}{$include};
2854 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2855 s/--\@\]/$delimiters{$format}[1]/g;
2858 &$convert_map( $conf->config($inc_file, $agentnum) );
2862 my $inc_tt = new Text::Template (
2864 SOURCE => [ map "$_\n", @inc_src ],
2865 DELIMITERS => $delimiters{$format},
2866 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2868 unless ( $inc_tt->compile() ) {
2869 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2870 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2874 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2876 $invoice_data{$include} =~ s/\n+$//
2877 if ($format eq 'latex');
2882 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2883 /invoice_lines\((\d*)\)/;
2884 $invoice_lines += $1 || scalar(@buf);
2887 die "no invoice_lines() functions in template?"
2888 if ( $format eq 'template' && !$wasfunc );
2891 warn Dumper(\@sections);
2892 if ($format eq 'template') {
2894 if ( $invoice_lines ) {
2895 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2896 $invoice_data{'total_pages'}++
2897 if scalar(@buf) % $invoice_lines;
2900 #setup subroutine for the template
2901 sub FS::cust_bill::_template::invoice_lines {
2902 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2904 scalar(@FS::cust_bill::_template::buf)
2905 ? shift @FS::cust_bill::_template::buf
2914 push @collect, split("\n",
2915 $text_template->fill_in( HASH => \%invoice_data,
2916 PACKAGE => 'FS::cust_bill::_template'
2919 $FS::cust_bill::_template::page++;
2921 map "$_\n", @collect;
2923 warn "filling in template for invoice ". $self->invnum. "\n"
2925 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2928 $text_template->fill_in(HASH => \%invoice_data);
2932 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2934 Returns an postscript invoice, as a scalar.
2936 Options can be passed as a hashref (recommended) or as a list of time, template
2937 and then any key/value pairs for any other options.
2939 I<time> an optional value used to control the printing of overdue messages. The
2940 default is now. It isn't the date of the invoice; that's the `_date' field.
2941 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2942 L<Time::Local> and L<Date::Parse> for conversion functions.
2944 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2951 my ($file, $lfile) = $self->print_latex(@_);
2952 my $ps = generate_ps($file);
2958 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2960 Returns an PDF invoice, as a scalar.
2962 Options can be passed as a hashref (recommended) or as a list of time, template
2963 and then any key/value pairs for any other options.
2965 I<time> an optional value used to control the printing of overdue messages. The
2966 default is now. It isn't the date of the invoice; that's the `_date' field.
2967 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2968 L<Time::Local> and L<Date::Parse> for conversion functions.
2970 I<template>, if specified, is the name of a suffix for alternate invoices.
2972 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2979 my ($file, $lfile) = $self->print_latex(@_);
2980 my $pdf = generate_pdf($file);
2986 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2988 Returns an HTML invoice, as a scalar.
2990 I<time> an optional value used to control the printing of overdue messages. The
2991 default is now. It isn't the date of the invoice; that's the `_date' field.
2992 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2993 L<Time::Local> and L<Date::Parse> for conversion functions.
2995 I<template>, if specified, is the name of a suffix for alternate invoices.
2997 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2999 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3000 when emailing the invoice as part of a multipart/related MIME email.
3008 %params = %{ shift() };
3010 $params{'time'} = shift;
3011 $params{'template'} = shift;
3012 $params{'cid'} = shift;
3015 $params{'format'} = 'html';
3017 $self->print_generic( %params );
3020 # quick subroutine for print_latex
3022 # There are ten characters that LaTeX treats as special characters, which
3023 # means that they do not simply typeset themselves:
3024 # # $ % & ~ _ ^ \ { }
3026 # TeX ignores blanks following an escaped character; if you want a blank (as
3027 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3031 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3032 $value =~ s/([<>])/\$$1\$/g;
3036 #utility methods for print_*
3038 sub _translate_old_latex_format {
3039 warn "_translate_old_latex_format called\n"
3046 if ( $line =~ /^%%Detail\s*$/ ) {
3048 push @template, q![@--!,
3049 q! foreach my $_tr_line (@detail_items) {!,
3050 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3051 q! $_tr_line->{'description'} .= !,
3052 q! "\\tabularnewline\n~~".!,
3053 q! join( "\\tabularnewline\n~~",!,
3054 q! @{$_tr_line->{'ext_description'}}!,
3058 while ( ( my $line_item_line = shift )
3059 !~ /^%%EndDetail\s*$/ ) {
3060 $line_item_line =~ s/'/\\'/g; # nice LTS
3061 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3062 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3063 push @template, " \$OUT .= '$line_item_line';";
3066 push @template, '}',
3069 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3071 push @template, '[@--',
3072 ' foreach my $_tr_line (@total_items) {';
3074 while ( ( my $total_item_line = shift )
3075 !~ /^%%EndTotalDetails\s*$/ ) {
3076 $total_item_line =~ s/'/\\'/g; # nice LTS
3077 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3078 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3079 push @template, " \$OUT .= '$total_item_line';";
3082 push @template, '}',
3086 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3087 push @template, $line;
3093 warn "$_\n" foreach @template;
3102 #check for an invoice-specific override
3103 return $self->invoice_terms if $self->invoice_terms;
3105 #check for a customer- specific override
3106 my $cust_main = $self->cust_main;
3107 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3109 #use configured default
3110 $conf->config('invoice_default_terms') || '';
3116 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3117 $duedate = $self->_date() + ( $1 * 86400 );
3124 $self->due_date ? time2str(shift, $self->due_date) : '';
3127 sub balance_due_msg {
3129 my $msg = 'Balance Due';
3130 return $msg unless $self->terms;
3131 if ( $self->due_date ) {
3132 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3133 } elsif ( $self->terms ) {
3134 $msg .= ' - '. $self->terms;
3139 sub balance_due_date {
3142 if ( $conf->exists('invoice_default_terms')
3143 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3144 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3149 =item invnum_date_pretty
3151 Returns a string with the invoice number and date, for example:
3152 "Invoice #54 (3/20/2008)"
3156 sub invnum_date_pretty {
3158 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3163 Returns a string with the date, for example: "3/20/2008"
3169 time2str('%x', $self->_date);
3172 use vars qw(%pkg_category_cache);
3173 sub _items_sections {
3176 my $summarypage = shift;
3178 my $extra_sections = shift;
3182 my %late_subtotal = ();
3185 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3188 my $usage = $cust_bill_pkg->usage;
3190 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3191 next if ( $display->summary && $summarypage );
3193 my $section = $display->section;
3194 my $type = $display->type;
3196 $not_tax{$section} = 1
3197 unless $cust_bill_pkg->pkgnum == 0;
3199 if ( $display->post_total && !$summarypage ) {
3200 if (! $type || $type eq 'S') {
3201 $late_subtotal{$section} += $cust_bill_pkg->setup
3202 if $cust_bill_pkg->setup != 0;
3206 $late_subtotal{$section} += $cust_bill_pkg->recur
3207 if $cust_bill_pkg->recur != 0;
3210 if ($type && $type eq 'R') {
3211 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3212 if $cust_bill_pkg->recur != 0;
3215 if ($type && $type eq 'U') {
3216 $late_subtotal{$section} += $usage
3217 unless scalar(@$extra_sections);
3222 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3224 if (! $type || $type eq 'S') {
3225 $subtotal{$section} += $cust_bill_pkg->setup
3226 if $cust_bill_pkg->setup != 0;
3230 $subtotal{$section} += $cust_bill_pkg->recur
3231 if $cust_bill_pkg->recur != 0;
3234 if ($type && $type eq 'R') {
3235 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3236 if $cust_bill_pkg->recur != 0;
3239 if ($type && $type eq 'U') {
3240 $subtotal{$section} += $usage
3241 unless scalar(@$extra_sections);
3250 %pkg_category_cache = ();
3252 push @$late, map { { 'description' => &{$escape}($_),
3253 'subtotal' => $late_subtotal{$_},
3255 'sort_weight' => ( _pkg_category($_)
3256 ? _pkg_category($_)->weight
3259 ((_pkg_category($_) && _pkg_category($_)->condense)
3260 ? $self->_condense_section($format)
3264 sort _sectionsort keys %late_subtotal;
3267 if ( $summarypage ) {
3268 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3269 map { $_->categoryname } qsearch('pkg_category', {});
3271 @sections = keys %subtotal;
3274 my @early = map { { 'description' => &{$escape}($_),
3275 'subtotal' => $subtotal{$_},
3276 'summarized' => $not_tax{$_} ? '' : 'Y',
3277 'tax_section' => $not_tax{$_} ? '' : 'Y',
3278 'sort_weight' => ( _pkg_category($_)
3279 ? _pkg_category($_)->weight
3282 ((_pkg_category($_) && _pkg_category($_)->condense)
3283 ? $self->_condense_section($format)
3288 push @early, @$extra_sections if $extra_sections;
3290 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3294 #helper subs for above
3297 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3301 my $categoryname = shift;
3302 $pkg_category_cache{$categoryname} ||=
3303 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3306 my %condensed_format = (
3307 'label' => [ qw( Description Qty Amount ) ],
3309 sub { shift->{description} },
3310 sub { shift->{quantity} },
3311 sub { shift->{amount} },
3313 'align' => [ qw( l r r ) ],
3314 'span' => [ qw( 5 1 1 ) ], # unitprices?
3315 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3318 sub _condense_section {
3319 my ( $self, $format ) = ( shift, shift );
3321 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3322 qw( description_generator
3325 total_line_generator
3330 sub _condensed_generator_defaults {
3331 my ( $self, $format ) = ( shift, shift );
3332 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3341 sub _condensed_header_generator {
3342 my ( $self, $format ) = ( shift, shift );
3344 my ( $f, $prefix, $suffix, $separator, $column ) =
3345 _condensed_generator_defaults($format);
3347 if ($format eq 'latex') {
3348 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3349 $suffix = "\\\\\n\\hline";
3352 sub { my ($d,$a,$s,$w) = @_;
3353 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3355 } elsif ( $format eq 'html' ) {
3356 $prefix = '<th></th>';
3360 sub { my ($d,$a,$s,$w) = @_;
3361 return qq!<th align="$html_align{$a}">$d</th>!;
3369 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3371 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3374 $prefix. join($separator, @result). $suffix;
3379 sub _condensed_description_generator {
3380 my ( $self, $format ) = ( shift, shift );
3382 my ( $f, $prefix, $suffix, $separator, $column ) =
3383 _condensed_generator_defaults($format);
3385 if ($format eq 'latex') {
3386 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3388 $separator = " & \n";
3390 sub { my ($d,$a,$s,$w) = @_;
3391 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3393 }elsif ( $format eq 'html' ) {
3394 $prefix = '"><td align="center"></td>';
3398 sub { my ($d,$a,$s,$w) = @_;
3399 return qq!<td align="$html_align{$a}">$d</td>!;
3407 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3408 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3409 map { $f->{$_}->[$i] } qw(align span width)
3413 $prefix. join( $separator, @result ). $suffix;
3418 sub _condensed_total_generator {
3419 my ( $self, $format ) = ( shift, shift );
3421 my ( $f, $prefix, $suffix, $separator, $column ) =
3422 _condensed_generator_defaults($format);
3425 if ($format eq 'latex') {
3428 $separator = " & \n";
3430 sub { my ($d,$a,$s,$w) = @_;
3431 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3433 }elsif ( $format eq 'html' ) {
3437 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3439 sub { my ($d,$a,$s,$w) = @_;
3440 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3449 # my $r = &{$f->{fields}->[$i]}(@args);
3450 # $r .= ' Total' unless $i;
3452 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3454 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3455 map { $f->{$_}->[$i] } qw(align span width)
3459 $prefix. join( $separator, @result ). $suffix;
3464 =item total_line_generator FORMAT
3466 Returns a coderef used for generation of invoice total line items for this
3467 usage_class. FORMAT is either html or latex
3471 # should not be used: will have issues with hash element names (description vs
3472 # total_item and amount vs total_amount -- another array of functions?
3474 sub _condensed_total_line_generator {
3475 my ( $self, $format ) = ( shift, shift );
3477 my ( $f, $prefix, $suffix, $separator, $column ) =
3478 _condensed_generator_defaults($format);
3481 if ($format eq 'latex') {
3484 $separator = " & \n";
3486 sub { my ($d,$a,$s,$w) = @_;
3487 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3489 }elsif ( $format eq 'html' ) {
3493 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3495 sub { my ($d,$a,$s,$w) = @_;
3496 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3505 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3507 &{$column}( &{$f->{fields}->[$i]}(@args),
3508 map { $f->{$_}->[$i] } qw(align span width)
3512 $prefix. join( $separator, @result ). $suffix;
3517 #sub _items_extra_usage_sections {
3519 # my $escape = shift;
3521 # my %sections = ();
3523 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3524 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3526 # next unless $cust_bill_pkg->pkgnum > 0;
3528 # foreach my $section ( keys %usage_class ) {
3530 # my $usage = $cust_bill_pkg->usage($section);
3532 # next unless $usage && $usage > 0;
3534 # $sections{$section} ||= 0;
3535 # $sections{$section} += $usage;
3541 # map { { 'description' => &{$escape}($_),
3542 # 'subtotal' => $sections{$_},
3543 # 'summarized' => '',
3544 # 'tax_section' => '',
3547 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3551 sub _items_extra_usage_sections {
3560 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3561 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3562 next unless $cust_bill_pkg->pkgnum > 0;
3564 foreach my $classnum ( keys %usage_class ) {
3565 my $section = $usage_class{$classnum}->classname;
3566 $classnums{$section} = $classnum;
3568 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3569 my $amount = $detail->amount;
3570 next unless $amount && $amount > 0;
3572 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3573 $sections{$section}{amount} += $amount; #subtotal
3574 $sections{$section}{calls}++;
3575 $sections{$section}{duration} += $detail->duration;
3577 my $desc = $detail->regionname;
3578 my $description = $desc;
3579 $description = substr($desc, 0, 50). '...'
3580 if $format eq 'latex' && length($desc) > 50;
3582 $lines{$section}{$desc} ||= {
3583 description => &{$escape}($description),
3584 #pkgpart => $part_pkg->pkgpart,
3585 pkgnum => $cust_bill_pkg->pkgnum,
3590 #unit_amount => $cust_bill_pkg->unitrecur,
3591 quantity => $cust_bill_pkg->quantity,
3592 product_code => 'N/A',
3593 ext_description => [],
3596 $lines{$section}{$desc}{amount} += $amount;
3597 $lines{$section}{$desc}{calls}++;
3598 $lines{$section}{$desc}{duration} += $detail->duration;
3604 my %sectionmap = ();
3605 foreach (keys %sections) {
3606 my $usage_class = $usage_class{$classnums{$_}};
3607 $sectionmap{$_} = { 'description' => &{$escape}($_),
3608 'amount' => $sections{$_}{amount}, #subtotal
3609 'calls' => $sections{$_}{calls},
3610 'duration' => $sections{$_}{duration},
3612 'tax_section' => '',
3613 'sort_weight' => $usage_class->weight,
3614 ( $usage_class->format
3615 ? ( map { $_ => $usage_class->$_($format) }
3616 qw( description_generator header_generator total_generator total_line_generator )
3623 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3627 foreach my $section ( keys %lines ) {
3628 foreach my $line ( keys %{$lines{$section}} ) {
3629 my $l = $lines{$section}{$line};
3630 $l->{section} = $sectionmap{$section};
3631 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3632 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3637 return(\@sections, \@lines);
3641 sub _items_svc_phone_sections {
3650 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3652 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3653 next unless $cust_bill_pkg->pkgnum > 0;
3655 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3657 my $phonenum = $detail->phonenum;
3658 next unless $phonenum;
3660 my $amount = $detail->amount;
3661 next unless $amount && $amount > 0;
3663 $sections{$phonenum} ||= { 'amount' => 0,
3666 'sort_weight' => -1,
3667 'phonenum' => $phonenum,
3669 $sections{$phonenum}{amount} += $amount; #subtotal
3670 $sections{$phonenum}{calls}++;
3671 $sections{$phonenum}{duration} += $detail->duration;
3673 my $desc = $detail->regionname;
3674 my $description = $desc;
3675 $description = substr($desc, 0, 50). '...'
3676 if $format eq 'latex' && length($desc) > 50;
3678 $lines{$phonenum}{$desc} ||= {
3679 description => &{$escape}($description),
3680 #pkgpart => $part_pkg->pkgpart,
3688 product_code => 'N/A',
3689 ext_description => [],
3692 $lines{$phonenum}{$desc}{amount} += $amount;
3693 $lines{$phonenum}{$desc}{calls}++;
3694 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3696 my $line = $usage_class{$detail->classnum}->classname;
3697 $sections{"$phonenum $line"} ||=
3701 'sort_weight' => $usage_class{$detail->classnum}->weight,
3702 'phonenum' => $phonenum,
3704 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3705 $sections{"$phonenum $line"}{calls}++;
3706 $sections{"$phonenum $line"}{duration} += $detail->duration;
3708 $lines{"$phonenum $line"}{$desc} ||= {
3709 description => &{$escape}($description),
3710 #pkgpart => $part_pkg->pkgpart,
3718 product_code => 'N/A',
3719 ext_description => [],
3722 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3723 $lines{"$phonenum $line"}{$desc}{calls}++;
3724 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3725 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3726 $detail->formatted('format' => $format);
3731 my %sectionmap = ();
3732 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3733 my $minimal = new FS::usage_class { format => 'minimal' }; #bleh
3734 foreach ( keys %sections ) {
3735 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3736 my $usage_class = $summary ? $simple : $minimal;
3737 my $ending = $summary ? ' usage charges' : '';
3738 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3739 'amount' => $sections{$_}{amount}, #subtotal
3740 'calls' => $sections{$_}{calls},
3741 'duration' => $sections{$_}{duration},
3743 'tax_section' => '',
3744 'phonenum' => $sections{$_}{phonenum},
3745 'sort_weight' => $sections{$_}{sort_weight},
3746 'post_total' => $summary, #inspire pagebreak
3748 ( map { $_ => $usage_class->$_($format) }
3749 qw( description_generator
3752 total_line_generator
3759 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3760 $a->{sort_weight} <=> $b->{sort_weight}
3765 foreach my $section ( keys %lines ) {
3766 foreach my $line ( keys %{$lines{$section}} ) {
3767 my $l = $lines{$section}{$line};
3768 $l->{section} = $sectionmap{$section};
3769 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3770 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3775 return(\@sections, \@lines);
3782 #my @display = scalar(@_)
3784 # : qw( _items_previous _items_pkg );
3785 # #: qw( _items_pkg );
3786 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3787 my @display = qw( _items_previous _items_pkg );
3790 foreach my $display ( @display ) {
3791 push @b, $self->$display(@_);
3796 sub _items_previous {
3798 my $cust_main = $self->cust_main;
3799 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3801 foreach ( @pr_cust_bill ) {
3803 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3804 ' ('. time2str('%x',$_->_date). ')',
3805 #'pkgpart' => 'N/A',
3807 'amount' => sprintf("%.2f", $_->owed),
3813 # 'description' => 'Previous Balance',
3814 # #'pkgpart' => 'N/A',
3815 # 'pkgnum' => 'N/A',
3816 # 'amount' => sprintf("%10.2f", $pr_total ),
3817 # 'ext_description' => [ map {
3818 # "Invoice ". $_->invnum.
3819 # " (". time2str("%x",$_->_date). ") ".
3820 # sprintf("%10.2f", $_->owed)
3821 # } @pr_cust_bill ],
3829 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3830 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3831 if ($options{section} && $options{section}->{condensed}) {
3833 local $Storable::canonical = 1;
3834 foreach ( @items ) {
3836 delete $item->{ref};
3837 delete $item->{ext_description};
3838 my $key = freeze($item);
3839 $itemshash{$key} ||= 0;
3840 $itemshash{$key} ++; # += $item->{quantity};
3842 @items = sort { $a->{description} cmp $b->{description} }
3843 map { my $i = thaw($_);
3844 $i->{quantity} = $itemshash{$_};
3846 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3855 return 0 unless $a cmp $b;
3856 return -1 if $b eq 'Tax';
3857 return 1 if $a eq 'Tax';
3858 return -1 if $b eq 'Other surcharges';
3859 return 1 if $a eq 'Other surcharges';
3865 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3866 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3869 sub _items_cust_bill_pkg {
3871 my $cust_bill_pkg = shift;
3874 my $format = $opt{format} || '';
3875 my $escape_function = $opt{escape_function} || sub { shift };
3876 my $format_function = $opt{format_function} || '';
3877 my $unsquelched = $opt{unsquelched} || '';
3878 my $section = $opt{section}->{description} if $opt{section};
3879 my $summary_page = $opt{summary_page} || '';
3882 my ($s, $r, $u) = ( undef, undef, undef );
3883 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3886 foreach ( $s, $r, ($opt{skip_usage} ? $u : () ) ) {
3887 if ( $_ && !$cust_bill_pkg->hidden ) {
3888 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3889 $_->{amount} =~ s/^\-0\.00$/0.00/;
3890 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3892 unless $_->{amount} == 0;
3897 foreach my $display ( grep { defined($section)
3898 ? $_->section eq $section
3901 grep { !$_->summary || !$summary_page }
3902 $cust_bill_pkg->cust_bill_pkg_display
3906 my $type = $display->type;
3908 my $desc = $cust_bill_pkg->desc;
3909 $desc = substr($desc, 0, 50). '...'
3910 if $format eq 'latex' && length($desc) > 50;
3912 my %details_opt = ( 'format' => $format,
3913 'escape_function' => $escape_function,
3914 'format_function' => $format_function,
3917 if ( $cust_bill_pkg->pkgnum > 0 ) {
3919 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3921 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3923 my $description = $desc;
3924 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3927 push @d, map &{$escape_function}($_),
3928 $cust_pkg->h_labels_short($self->_date)
3929 unless $cust_pkg->part_pkg->hide_svc_detail
3930 || $cust_bill_pkg->hidden;
3931 push @d, $cust_bill_pkg->details(%details_opt)
3932 if $cust_bill_pkg->recur == 0;
3934 if ( $cust_bill_pkg->hidden ) {
3935 $s->{amount} += $cust_bill_pkg->setup;
3936 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3937 push @{ $s->{ext_description} }, @d;
3940 description => $description,
3941 #pkgpart => $part_pkg->pkgpart,
3942 pkgnum => $cust_bill_pkg->pkgnum,
3943 amount => $cust_bill_pkg->setup,
3944 unit_amount => $cust_bill_pkg->unitsetup,
3945 quantity => $cust_bill_pkg->quantity,
3946 ext_description => \@d,
3952 if ( $cust_bill_pkg->recur != 0 &&
3953 ( !$type || $type eq 'R' || $type eq 'U' )
3957 my $is_summary = $display->summary;
3958 my $description = ($is_summary && $type && $type eq 'U')
3959 ? "Usage charges" : $desc;
3961 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3962 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3963 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3968 #at least until cust_bill_pkg has "past" ranges in addition to
3969 #the "future" sdate/edate ones... see #3032
3970 my @dates = ( $self->_date );
3971 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3972 push @dates, $prev->sdate if $prev;
3974 push @d, map &{$escape_function}($_),
3975 $cust_pkg->h_labels_short(@dates)
3976 #$cust_bill_pkg->edate,
3977 #$cust_bill_pkg->sdate)
3978 unless $cust_pkg->part_pkg->hide_svc_detail
3979 || $cust_bill_pkg->itemdesc
3980 || $cust_bill_pkg->hidden
3981 || $is_summary && $type && $type eq 'U';
3983 push @d, $cust_bill_pkg->details(%details_opt)
3984 unless ($is_summary || $type && $type eq 'R');
3988 $amount = $cust_bill_pkg->recur;
3989 }elsif($type eq 'R') {
3990 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3991 }elsif($type eq 'U') {
3992 $amount = $cust_bill_pkg->usage;
3995 if ( !$type || $type eq 'R' ) {
3997 if ( $cust_bill_pkg->hidden ) {
3998 $r->{amount} += $amount;
3999 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4000 push @{ $r->{ext_description} }, @d;
4003 description => $description,
4004 #pkgpart => $part_pkg->pkgpart,
4005 pkgnum => $cust_bill_pkg->pkgnum,
4007 unit_amount => $cust_bill_pkg->unitrecur,
4008 quantity => $cust_bill_pkg->quantity,
4009 ext_description => \@d,
4013 } elsif ( $amount ) { # && $type eq 'U'
4015 if ( $cust_bill_pkg->hidden ) {
4016 $u->{amount} += $amount;
4017 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4018 push @{ $u->{ext_description} }, @d;
4021 description => $description,
4022 #pkgpart => $part_pkg->pkgpart,
4023 pkgnum => $cust_bill_pkg->pkgnum,
4025 unit_amount => $cust_bill_pkg->unitrecur,
4026 quantity => $cust_bill_pkg->quantity,
4027 ext_description => \@d,
4033 } # recurring or usage with recurring charge
4035 } else { #pkgnum tax or one-shot line item (??)
4037 if ( $cust_bill_pkg->setup != 0 ) {
4039 'description' => $desc,
4040 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4043 if ( $cust_bill_pkg->recur != 0 ) {
4045 'description' => "$desc (".
4046 time2str("%x", $cust_bill_pkg->sdate). ' - '.
4047 time2str("%x", $cust_bill_pkg->edate). ')',
4048 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4058 foreach ( $s, $r, ($opt{skip_usage} ? $u : () ) ) {
4060 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4061 $_->{amount} =~ s/^\-0\.00$/0.00/;
4062 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4064 unless $_->{amount} == 0;
4072 sub _items_credits {
4073 my( $self, %opt ) = @_;
4074 my $trim_len = $opt{'trim_len'} || 60;
4078 foreach ( $self->cust_credited ) {
4080 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4082 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4083 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4084 $reason = " ($reason) " if $reason;
4087 #'description' => 'Credit ref\#'. $_->crednum.
4088 # " (". time2str("%x",$_->cust_credit->_date) .")".
4090 'description' => 'Credit applied '.
4091 time2str("%x",$_->cust_credit->_date). $reason,
4092 'amount' => sprintf("%.2f",$_->amount),
4100 sub _items_payments {
4104 #get & print payments
4105 foreach ( $self->cust_bill_pay ) {
4107 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4110 'description' => "Payment received ".
4111 time2str("%x",$_->cust_pay->_date ),
4112 'amount' => sprintf("%.2f", $_->amount )
4120 =item call_details [ OPTION => VALUE ... ]
4122 Returns an array of CSV strings representing the call details for this invoice
4123 The only option available is the boolean prepend_billed_number
4128 my ($self, %opt) = @_;
4130 my $format_function = sub { shift };
4132 if ($opt{prepend_billed_number}) {
4133 $format_function = sub {
4137 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4142 my @details = map { $_->details( 'format_function' => $format_function,
4143 'escape_function' => sub{ return() },
4147 $self->cust_bill_pkg;
4148 my $header = $details[0];
4149 ( $header, grep { $_ ne $header } @details );
4159 =item process_reprint
4163 sub process_reprint {
4164 process_re_X('print', @_);
4167 =item process_reemail
4171 sub process_reemail {
4172 process_re_X('email', @_);
4180 process_re_X('fax', @_);
4188 process_re_X('ftp', @_);
4195 sub process_respool {
4196 process_re_X('spool', @_);
4199 use Storable qw(thaw);
4203 my( $method, $job ) = ( shift, shift );
4204 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4206 my $param = thaw(decode_base64(shift));
4207 warn Dumper($param) if $DEBUG;
4218 my($method, $job, %param ) = @_;
4220 warn "re_X $method for job $job with param:\n".
4221 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4224 #some false laziness w/search/cust_bill.html
4226 my $orderby = 'ORDER BY cust_bill._date';
4228 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
4230 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4232 my @cust_bill = qsearch( {
4233 #'select' => "cust_bill.*",
4234 'table' => 'cust_bill',
4235 'addl_from' => $addl_from,
4237 'extra_sql' => $extra_sql,
4238 'order_by' => $orderby,
4242 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4244 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4247 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4248 foreach my $cust_bill ( @cust_bill ) {
4249 $cust_bill->$method();
4251 if ( $job ) { #progressbar foo
4253 if ( time - $min_sec > $last ) {
4254 my $error = $job->update_statustext(
4255 int( 100 * $num / scalar(@cust_bill) )
4257 die $error if $error;
4268 =head1 CLASS METHODS
4274 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4280 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4285 Returns an SQL fragment to retreive the net amount (charged minus credited).
4291 'charged - '. $class->credited_sql;
4296 Returns an SQL fragment to retreive the amount paid against this invoice.
4302 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4303 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4308 Returns an SQL fragment to retreive the amount credited against this invoice.
4314 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4315 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4318 =item search_sql HASHREF
4320 Class method which returns an SQL WHERE fragment to search for parameters
4321 specified in HASHREF. Valid parameters are
4327 List reference of start date, end date, as UNIX timestamps.
4337 List reference of charged limits (exclusive).
4341 List reference of charged limits (exclusive).
4345 flag, return open invoices only
4349 flag, return net invoices only
4353 =item newest_percust
4357 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4362 my($class, $param) = @_;
4364 warn "$me search_sql called with params: \n".
4365 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4371 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4372 push @search, "cust_main.agentnum = $1";
4376 if ( $param->{_date} ) {
4377 my($beginning, $ending) = @{$param->{_date}};
4379 push @search, "cust_bill._date >= $beginning",
4380 "cust_bill._date < $ending";
4384 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4385 push @search, "cust_bill.invnum >= $1";
4387 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4388 push @search, "cust_bill.invnum <= $1";
4392 if ( $param->{charged} ) {
4393 my @charged = ref($param->{charged})
4394 ? @{ $param->{charged} }
4395 : ($param->{charged});
4397 push @search, map { s/^charged/cust_bill.charged/; $_; }
4401 my $owed_sql = FS::cust_bill->owed_sql;
4404 if ( $param->{owed} ) {
4405 my @owed = ref($param->{owed})
4406 ? @{ $param->{owed} }
4408 push @search, map { s/^owed/$owed_sql/; $_; }
4413 push @search, "0 != $owed_sql"
4414 if $param->{'open'};
4415 push @search, '0 != '. FS::cust_bill->net_sql
4419 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4420 if $param->{'days'};
4423 if ( $param->{'newest_percust'} ) {
4425 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4426 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4428 my @newest_where = map { my $x = $_;
4429 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4432 grep ! /^cust_main./, @search;
4433 my $newest_where = scalar(@newest_where)
4434 ? ' AND '. join(' AND ', @newest_where)
4438 push @search, "cust_bill._date = (
4439 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4440 WHERE newest_cust_bill.custnum = cust_bill.custnum
4446 #agent virtualization
4447 my $curuser = $FS::CurrentUser::CurrentUser;
4448 if ( $curuser->username eq 'fs_queue'
4449 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4451 my $newuser = qsearchs('access_user', {
4452 'username' => $username,
4456 $curuser = $newuser;
4458 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4461 push @search, $curuser->agentnums_sql;
4463 join(' AND ', @search );
4475 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4476 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base