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 );
2890 if ($format eq 'template') {
2892 if ( $invoice_lines ) {
2893 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2894 $invoice_data{'total_pages'}++
2895 if scalar(@buf) % $invoice_lines;
2898 #setup subroutine for the template
2899 sub FS::cust_bill::_template::invoice_lines {
2900 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2902 scalar(@FS::cust_bill::_template::buf)
2903 ? shift @FS::cust_bill::_template::buf
2912 push @collect, split("\n",
2913 $text_template->fill_in( HASH => \%invoice_data,
2914 PACKAGE => 'FS::cust_bill::_template'
2917 $FS::cust_bill::_template::page++;
2919 map "$_\n", @collect;
2921 warn "filling in template for invoice ". $self->invnum. "\n"
2923 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2926 $text_template->fill_in(HASH => \%invoice_data);
2930 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2932 Returns an postscript invoice, as a scalar.
2934 Options can be passed as a hashref (recommended) or as a list of time, template
2935 and then any key/value pairs for any other options.
2937 I<time> an optional value used to control the printing of overdue messages. The
2938 default is now. It isn't the date of the invoice; that's the `_date' field.
2939 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2940 L<Time::Local> and L<Date::Parse> for conversion functions.
2942 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2949 my ($file, $lfile) = $self->print_latex(@_);
2950 my $ps = generate_ps($file);
2956 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2958 Returns an PDF invoice, as a scalar.
2960 Options can be passed as a hashref (recommended) or as a list of time, template
2961 and then any key/value pairs for any other options.
2963 I<time> an optional value used to control the printing of overdue messages. The
2964 default is now. It isn't the date of the invoice; that's the `_date' field.
2965 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2966 L<Time::Local> and L<Date::Parse> for conversion functions.
2968 I<template>, if specified, is the name of a suffix for alternate invoices.
2970 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2977 my ($file, $lfile) = $self->print_latex(@_);
2978 my $pdf = generate_pdf($file);
2984 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2986 Returns an HTML invoice, as a scalar.
2988 I<time> an optional value used to control the printing of overdue messages. The
2989 default is now. It isn't the date of the invoice; that's the `_date' field.
2990 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2991 L<Time::Local> and L<Date::Parse> for conversion functions.
2993 I<template>, if specified, is the name of a suffix for alternate invoices.
2995 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2997 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2998 when emailing the invoice as part of a multipart/related MIME email.
3006 %params = %{ shift() };
3008 $params{'time'} = shift;
3009 $params{'template'} = shift;
3010 $params{'cid'} = shift;
3013 $params{'format'} = 'html';
3015 $self->print_generic( %params );
3018 # quick subroutine for print_latex
3020 # There are ten characters that LaTeX treats as special characters, which
3021 # means that they do not simply typeset themselves:
3022 # # $ % & ~ _ ^ \ { }
3024 # TeX ignores blanks following an escaped character; if you want a blank (as
3025 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3029 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3030 $value =~ s/([<>])/\$$1\$/g;
3034 #utility methods for print_*
3036 sub _translate_old_latex_format {
3037 warn "_translate_old_latex_format called\n"
3044 if ( $line =~ /^%%Detail\s*$/ ) {
3046 push @template, q![@--!,
3047 q! foreach my $_tr_line (@detail_items) {!,
3048 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3049 q! $_tr_line->{'description'} .= !,
3050 q! "\\tabularnewline\n~~".!,
3051 q! join( "\\tabularnewline\n~~",!,
3052 q! @{$_tr_line->{'ext_description'}}!,
3056 while ( ( my $line_item_line = shift )
3057 !~ /^%%EndDetail\s*$/ ) {
3058 $line_item_line =~ s/'/\\'/g; # nice LTS
3059 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3060 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3061 push @template, " \$OUT .= '$line_item_line';";
3064 push @template, '}',
3067 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3069 push @template, '[@--',
3070 ' foreach my $_tr_line (@total_items) {';
3072 while ( ( my $total_item_line = shift )
3073 !~ /^%%EndTotalDetails\s*$/ ) {
3074 $total_item_line =~ s/'/\\'/g; # nice LTS
3075 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3076 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3077 push @template, " \$OUT .= '$total_item_line';";
3080 push @template, '}',
3084 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3085 push @template, $line;
3091 warn "$_\n" foreach @template;
3100 #check for an invoice-specific override
3101 return $self->invoice_terms if $self->invoice_terms;
3103 #check for a customer- specific override
3104 my $cust_main = $self->cust_main;
3105 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3107 #use configured default
3108 $conf->config('invoice_default_terms') || '';
3114 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3115 $duedate = $self->_date() + ( $1 * 86400 );
3122 $self->due_date ? time2str(shift, $self->due_date) : '';
3125 sub balance_due_msg {
3127 my $msg = 'Balance Due';
3128 return $msg unless $self->terms;
3129 if ( $self->due_date ) {
3130 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3131 } elsif ( $self->terms ) {
3132 $msg .= ' - '. $self->terms;
3137 sub balance_due_date {
3140 if ( $conf->exists('invoice_default_terms')
3141 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3142 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3147 =item invnum_date_pretty
3149 Returns a string with the invoice number and date, for example:
3150 "Invoice #54 (3/20/2008)"
3154 sub invnum_date_pretty {
3156 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3161 Returns a string with the date, for example: "3/20/2008"
3167 time2str('%x', $self->_date);
3170 use vars qw(%pkg_category_cache);
3171 sub _items_sections {
3174 my $summarypage = shift;
3176 my $extra_sections = shift;
3180 my %late_subtotal = ();
3183 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3186 my $usage = $cust_bill_pkg->usage;
3188 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3189 next if ( $display->summary && $summarypage );
3191 my $section = $display->section;
3192 my $type = $display->type;
3194 $not_tax{$section} = 1
3195 unless $cust_bill_pkg->pkgnum == 0;
3197 if ( $display->post_total && !$summarypage ) {
3198 if (! $type || $type eq 'S') {
3199 $late_subtotal{$section} += $cust_bill_pkg->setup
3200 if $cust_bill_pkg->setup != 0;
3204 $late_subtotal{$section} += $cust_bill_pkg->recur
3205 if $cust_bill_pkg->recur != 0;
3208 if ($type && $type eq 'R') {
3209 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3210 if $cust_bill_pkg->recur != 0;
3213 if ($type && $type eq 'U') {
3214 $late_subtotal{$section} += $usage
3215 unless scalar(@$extra_sections);
3220 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3222 if (! $type || $type eq 'S') {
3223 $subtotal{$section} += $cust_bill_pkg->setup
3224 if $cust_bill_pkg->setup != 0;
3228 $subtotal{$section} += $cust_bill_pkg->recur
3229 if $cust_bill_pkg->recur != 0;
3232 if ($type && $type eq 'R') {
3233 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3234 if $cust_bill_pkg->recur != 0;
3237 if ($type && $type eq 'U') {
3238 $subtotal{$section} += $usage
3239 unless scalar(@$extra_sections);
3248 %pkg_category_cache = ();
3250 push @$late, map { { 'description' => &{$escape}($_),
3251 'subtotal' => $late_subtotal{$_},
3253 'sort_weight' => ( _pkg_category($_)
3254 ? _pkg_category($_)->weight
3257 ((_pkg_category($_) && _pkg_category($_)->condense)
3258 ? $self->_condense_section($format)
3262 sort _sectionsort keys %late_subtotal;
3265 if ( $summarypage ) {
3266 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3267 map { $_->categoryname } qsearch('pkg_category', {});
3269 @sections = keys %subtotal;
3272 my @early = map { { 'description' => &{$escape}($_),
3273 'subtotal' => $subtotal{$_},
3274 'summarized' => $not_tax{$_} ? '' : 'Y',
3275 'tax_section' => $not_tax{$_} ? '' : 'Y',
3276 'sort_weight' => ( _pkg_category($_)
3277 ? _pkg_category($_)->weight
3280 ((_pkg_category($_) && _pkg_category($_)->condense)
3281 ? $self->_condense_section($format)
3286 push @early, @$extra_sections if $extra_sections;
3288 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3292 #helper subs for above
3295 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3299 my $categoryname = shift;
3300 $pkg_category_cache{$categoryname} ||=
3301 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3304 my %condensed_format = (
3305 'label' => [ qw( Description Qty Amount ) ],
3307 sub { shift->{description} },
3308 sub { shift->{quantity} },
3309 sub { shift->{amount} },
3311 'align' => [ qw( l r r ) ],
3312 'span' => [ qw( 5 1 1 ) ], # unitprices?
3313 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3316 sub _condense_section {
3317 my ( $self, $format ) = ( shift, shift );
3319 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3320 qw( description_generator
3323 total_line_generator
3328 sub _condensed_generator_defaults {
3329 my ( $self, $format ) = ( shift, shift );
3330 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3339 sub _condensed_header_generator {
3340 my ( $self, $format ) = ( shift, shift );
3342 my ( $f, $prefix, $suffix, $separator, $column ) =
3343 _condensed_generator_defaults($format);
3345 if ($format eq 'latex') {
3346 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3347 $suffix = "\\\\\n\\hline";
3350 sub { my ($d,$a,$s,$w) = @_;
3351 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3353 } elsif ( $format eq 'html' ) {
3354 $prefix = '<th></th>';
3358 sub { my ($d,$a,$s,$w) = @_;
3359 return qq!<th align="$html_align{$a}">$d</th>!;
3367 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3369 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3372 $prefix. join($separator, @result). $suffix;
3377 sub _condensed_description_generator {
3378 my ( $self, $format ) = ( shift, shift );
3380 my ( $f, $prefix, $suffix, $separator, $column ) =
3381 _condensed_generator_defaults($format);
3383 if ($format eq 'latex') {
3384 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3386 $separator = " & \n";
3388 sub { my ($d,$a,$s,$w) = @_;
3389 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3391 }elsif ( $format eq 'html' ) {
3392 $prefix = '"><td align="center"></td>';
3396 sub { my ($d,$a,$s,$w) = @_;
3397 return qq!<td align="$html_align{$a}">$d</td>!;
3405 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3406 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3407 map { $f->{$_}->[$i] } qw(align span width)
3411 $prefix. join( $separator, @result ). $suffix;
3416 sub _condensed_total_generator {
3417 my ( $self, $format ) = ( shift, shift );
3419 my ( $f, $prefix, $suffix, $separator, $column ) =
3420 _condensed_generator_defaults($format);
3423 if ($format eq 'latex') {
3426 $separator = " & \n";
3428 sub { my ($d,$a,$s,$w) = @_;
3429 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3431 }elsif ( $format eq 'html' ) {
3435 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3437 sub { my ($d,$a,$s,$w) = @_;
3438 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3447 # my $r = &{$f->{fields}->[$i]}(@args);
3448 # $r .= ' Total' unless $i;
3450 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3452 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3453 map { $f->{$_}->[$i] } qw(align span width)
3457 $prefix. join( $separator, @result ). $suffix;
3462 =item total_line_generator FORMAT
3464 Returns a coderef used for generation of invoice total line items for this
3465 usage_class. FORMAT is either html or latex
3469 # should not be used: will have issues with hash element names (description vs
3470 # total_item and amount vs total_amount -- another array of functions?
3472 sub _condensed_total_line_generator {
3473 my ( $self, $format ) = ( shift, shift );
3475 my ( $f, $prefix, $suffix, $separator, $column ) =
3476 _condensed_generator_defaults($format);
3479 if ($format eq 'latex') {
3482 $separator = " & \n";
3484 sub { my ($d,$a,$s,$w) = @_;
3485 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3487 }elsif ( $format eq 'html' ) {
3491 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3493 sub { my ($d,$a,$s,$w) = @_;
3494 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3503 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3505 &{$column}( &{$f->{fields}->[$i]}(@args),
3506 map { $f->{$_}->[$i] } qw(align span width)
3510 $prefix. join( $separator, @result ). $suffix;
3515 #sub _items_extra_usage_sections {
3517 # my $escape = shift;
3519 # my %sections = ();
3521 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3522 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3524 # next unless $cust_bill_pkg->pkgnum > 0;
3526 # foreach my $section ( keys %usage_class ) {
3528 # my $usage = $cust_bill_pkg->usage($section);
3530 # next unless $usage && $usage > 0;
3532 # $sections{$section} ||= 0;
3533 # $sections{$section} += $usage;
3539 # map { { 'description' => &{$escape}($_),
3540 # 'subtotal' => $sections{$_},
3541 # 'summarized' => '',
3542 # 'tax_section' => '',
3545 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3549 sub _items_extra_usage_sections {
3558 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3559 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3560 next unless $cust_bill_pkg->pkgnum > 0;
3562 foreach my $classnum ( keys %usage_class ) {
3563 my $section = $usage_class{$classnum}->classname;
3564 $classnums{$section} = $classnum;
3566 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3567 my $amount = $detail->amount;
3568 next unless $amount && $amount > 0;
3570 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3571 $sections{$section}{amount} += $amount; #subtotal
3572 $sections{$section}{calls}++;
3573 $sections{$section}{duration} += $detail->duration;
3575 my $desc = $detail->regionname;
3576 my $description = $desc;
3577 $description = substr($desc, 0, 50). '...'
3578 if $format eq 'latex' && length($desc) > 50;
3580 $lines{$section}{$desc} ||= {
3581 description => &{$escape}($description),
3582 #pkgpart => $part_pkg->pkgpart,
3583 pkgnum => $cust_bill_pkg->pkgnum,
3588 #unit_amount => $cust_bill_pkg->unitrecur,
3589 quantity => $cust_bill_pkg->quantity,
3590 product_code => 'N/A',
3591 ext_description => [],
3594 $lines{$section}{$desc}{amount} += $amount;
3595 $lines{$section}{$desc}{calls}++;
3596 $lines{$section}{$desc}{duration} += $detail->duration;
3602 my %sectionmap = ();
3603 foreach (keys %sections) {
3604 my $usage_class = $usage_class{$classnums{$_}};
3605 $sectionmap{$_} = { 'description' => &{$escape}($_),
3606 'amount' => $sections{$_}{amount}, #subtotal
3607 'calls' => $sections{$_}{calls},
3608 'duration' => $sections{$_}{duration},
3610 'tax_section' => '',
3611 'sort_weight' => $usage_class->weight,
3612 ( $usage_class->format
3613 ? ( map { $_ => $usage_class->$_($format) }
3614 qw( description_generator header_generator total_generator total_line_generator )
3621 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3625 foreach my $section ( keys %lines ) {
3626 foreach my $line ( keys %{$lines{$section}} ) {
3627 my $l = $lines{$section}{$line};
3628 $l->{section} = $sectionmap{$section};
3629 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3630 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3635 return(\@sections, \@lines);
3639 sub _items_svc_phone_sections {
3648 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3650 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3651 next unless $cust_bill_pkg->pkgnum > 0;
3653 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3655 my $phonenum = $detail->phonenum;
3656 next unless $phonenum;
3658 my $amount = $detail->amount;
3659 next unless $amount && $amount > 0;
3661 $sections{$phonenum} ||= { 'amount' => 0,
3664 'sort_weight' => -1,
3665 'phonenum' => $phonenum,
3667 $sections{$phonenum}{amount} += $amount; #subtotal
3668 $sections{$phonenum}{calls}++;
3669 $sections{$phonenum}{duration} += $detail->duration;
3671 my $desc = $detail->regionname;
3672 my $description = $desc;
3673 $description = substr($desc, 0, 50). '...'
3674 if $format eq 'latex' && length($desc) > 50;
3676 $lines{$phonenum}{$desc} ||= {
3677 description => &{$escape}($description),
3678 #pkgpart => $part_pkg->pkgpart,
3686 product_code => 'N/A',
3687 ext_description => [],
3690 $lines{$phonenum}{$desc}{amount} += $amount;
3691 $lines{$phonenum}{$desc}{calls}++;
3692 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3694 my $line = $usage_class{$detail->classnum}->classname;
3695 $sections{"$phonenum $line"} ||=
3699 'sort_weight' => $usage_class{$detail->classnum}->weight,
3700 'phonenum' => $phonenum,
3702 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3703 $sections{"$phonenum $line"}{calls}++;
3704 $sections{"$phonenum $line"}{duration} += $detail->duration;
3706 $lines{"$phonenum $line"}{$desc} ||= {
3707 description => &{$escape}($description),
3708 #pkgpart => $part_pkg->pkgpart,
3716 product_code => 'N/A',
3717 ext_description => [],
3720 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3721 $lines{"$phonenum $line"}{$desc}{calls}++;
3722 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3723 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3724 $detail->formatted('format' => $format);
3729 my %sectionmap = ();
3730 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3731 my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3732 foreach ( keys %sections ) {
3733 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3734 my $usage_class = $summary ? $simple : $usage_simple;
3735 my $ending = $summary ? ' usage charges' : '';
3736 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3737 'amount' => $sections{$_}{amount}, #subtotal
3738 'calls' => $sections{$_}{calls},
3739 'duration' => $sections{$_}{duration},
3741 'tax_section' => '',
3742 'phonenum' => $sections{$_}{phonenum},
3743 'sort_weight' => $sections{$_}{sort_weight},
3744 'post_total' => $summary, #inspire pagebreak
3746 ( map { $_ => $usage_class->$_($format) }
3747 qw( description_generator
3750 total_line_generator
3757 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3758 $a->{sort_weight} <=> $b->{sort_weight}
3763 foreach my $section ( keys %lines ) {
3764 foreach my $line ( keys %{$lines{$section}} ) {
3765 my $l = $lines{$section}{$line};
3766 $l->{section} = $sectionmap{$section};
3767 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3768 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3773 return(\@sections, \@lines);
3780 #my @display = scalar(@_)
3782 # : qw( _items_previous _items_pkg );
3783 # #: qw( _items_pkg );
3784 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3785 my @display = qw( _items_previous _items_pkg );
3788 foreach my $display ( @display ) {
3789 push @b, $self->$display(@_);
3794 sub _items_previous {
3796 my $cust_main = $self->cust_main;
3797 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3799 foreach ( @pr_cust_bill ) {
3801 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3802 ' ('. time2str('%x',$_->_date). ')',
3803 #'pkgpart' => 'N/A',
3805 'amount' => sprintf("%.2f", $_->owed),
3811 # 'description' => 'Previous Balance',
3812 # #'pkgpart' => 'N/A',
3813 # 'pkgnum' => 'N/A',
3814 # 'amount' => sprintf("%10.2f", $pr_total ),
3815 # 'ext_description' => [ map {
3816 # "Invoice ". $_->invnum.
3817 # " (". time2str("%x",$_->_date). ") ".
3818 # sprintf("%10.2f", $_->owed)
3819 # } @pr_cust_bill ],
3827 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3828 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3829 if ($options{section} && $options{section}->{condensed}) {
3831 local $Storable::canonical = 1;
3832 foreach ( @items ) {
3834 delete $item->{ref};
3835 delete $item->{ext_description};
3836 my $key = freeze($item);
3837 $itemshash{$key} ||= 0;
3838 $itemshash{$key} ++; # += $item->{quantity};
3840 @items = sort { $a->{description} cmp $b->{description} }
3841 map { my $i = thaw($_);
3842 $i->{quantity} = $itemshash{$_};
3844 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3853 return 0 unless $a cmp $b;
3854 return -1 if $b eq 'Tax';
3855 return 1 if $a eq 'Tax';
3856 return -1 if $b eq 'Other surcharges';
3857 return 1 if $a eq 'Other surcharges';
3863 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3864 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3867 sub _items_cust_bill_pkg {
3869 my $cust_bill_pkg = shift;
3872 my $format = $opt{format} || '';
3873 my $escape_function = $opt{escape_function} || sub { shift };
3874 my $format_function = $opt{format_function} || '';
3875 my $unsquelched = $opt{unsquelched} || '';
3876 my $section = $opt{section}->{description} if $opt{section};
3877 my $summary_page = $opt{summary_page} || '';
3880 my ($s, $r, $u) = ( undef, undef, undef );
3881 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3884 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3885 if ( $_ && !$cust_bill_pkg->hidden ) {
3886 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3887 $_->{amount} =~ s/^\-0\.00$/0.00/;
3888 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3890 unless $_->{amount} == 0;
3895 foreach my $display ( grep { defined($section)
3896 ? $_->section eq $section
3899 grep { !$_->summary || !$summary_page }
3900 $cust_bill_pkg->cust_bill_pkg_display
3904 my $type = $display->type;
3906 my $desc = $cust_bill_pkg->desc;
3907 $desc = substr($desc, 0, 50). '...'
3908 if $format eq 'latex' && length($desc) > 50;
3910 my %details_opt = ( 'format' => $format,
3911 'escape_function' => $escape_function,
3912 'format_function' => $format_function,
3915 if ( $cust_bill_pkg->pkgnum > 0 ) {
3917 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3919 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3921 my $description = $desc;
3922 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3925 push @d, map &{$escape_function}($_),
3926 $cust_pkg->h_labels_short($self->_date)
3927 unless $cust_pkg->part_pkg->hide_svc_detail
3928 || $cust_bill_pkg->hidden;
3929 push @d, $cust_bill_pkg->details(%details_opt)
3930 if $cust_bill_pkg->recur == 0;
3932 if ( $cust_bill_pkg->hidden ) {
3933 $s->{amount} += $cust_bill_pkg->setup;
3934 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3935 push @{ $s->{ext_description} }, @d;
3938 description => $description,
3939 #pkgpart => $part_pkg->pkgpart,
3940 pkgnum => $cust_bill_pkg->pkgnum,
3941 amount => $cust_bill_pkg->setup,
3942 unit_amount => $cust_bill_pkg->unitsetup,
3943 quantity => $cust_bill_pkg->quantity,
3944 ext_description => \@d,
3950 if ( $cust_bill_pkg->recur != 0 &&
3951 ( !$type || $type eq 'R' || $type eq 'U' )
3955 my $is_summary = $display->summary;
3956 my $description = ($is_summary && $type && $type eq 'U')
3957 ? "Usage charges" : $desc;
3959 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3960 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3961 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3966 #at least until cust_bill_pkg has "past" ranges in addition to
3967 #the "future" sdate/edate ones... see #3032
3968 my @dates = ( $self->_date );
3969 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3970 push @dates, $prev->sdate if $prev;
3972 push @d, map &{$escape_function}($_),
3973 $cust_pkg->h_labels_short(@dates)
3974 #$cust_bill_pkg->edate,
3975 #$cust_bill_pkg->sdate)
3976 unless $cust_pkg->part_pkg->hide_svc_detail
3977 || $cust_bill_pkg->itemdesc
3978 || $cust_bill_pkg->hidden
3979 || $is_summary && $type && $type eq 'U';
3981 push @d, $cust_bill_pkg->details(%details_opt)
3982 unless ($is_summary || $type && $type eq 'R');
3986 $amount = $cust_bill_pkg->recur;
3987 }elsif($type eq 'R') {
3988 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3989 }elsif($type eq 'U') {
3990 $amount = $cust_bill_pkg->usage;
3993 if ( !$type || $type eq 'R' ) {
3995 if ( $cust_bill_pkg->hidden ) {
3996 $r->{amount} += $amount;
3997 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3998 push @{ $r->{ext_description} }, @d;
4001 description => $description,
4002 #pkgpart => $part_pkg->pkgpart,
4003 pkgnum => $cust_bill_pkg->pkgnum,
4005 unit_amount => $cust_bill_pkg->unitrecur,
4006 quantity => $cust_bill_pkg->quantity,
4007 ext_description => \@d,
4011 } elsif ( $amount ) { # && $type eq 'U'
4013 if ( $cust_bill_pkg->hidden ) {
4014 $u->{amount} += $amount;
4015 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4016 push @{ $u->{ext_description} }, @d;
4019 description => $description,
4020 #pkgpart => $part_pkg->pkgpart,
4021 pkgnum => $cust_bill_pkg->pkgnum,
4023 unit_amount => $cust_bill_pkg->unitrecur,
4024 quantity => $cust_bill_pkg->quantity,
4025 ext_description => \@d,
4031 } # recurring or usage with recurring charge
4033 } else { #pkgnum tax or one-shot line item (??)
4035 if ( $cust_bill_pkg->setup != 0 ) {
4037 'description' => $desc,
4038 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4041 if ( $cust_bill_pkg->recur != 0 ) {
4043 'description' => "$desc (".
4044 time2str("%x", $cust_bill_pkg->sdate). ' - '.
4045 time2str("%x", $cust_bill_pkg->edate). ')',
4046 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4056 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4058 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4059 $_->{amount} =~ s/^\-0\.00$/0.00/;
4060 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4062 unless $_->{amount} == 0;
4070 sub _items_credits {
4071 my( $self, %opt ) = @_;
4072 my $trim_len = $opt{'trim_len'} || 60;
4076 foreach ( $self->cust_credited ) {
4078 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4080 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4081 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4082 $reason = " ($reason) " if $reason;
4085 #'description' => 'Credit ref\#'. $_->crednum.
4086 # " (". time2str("%x",$_->cust_credit->_date) .")".
4088 'description' => 'Credit applied '.
4089 time2str("%x",$_->cust_credit->_date). $reason,
4090 'amount' => sprintf("%.2f",$_->amount),
4098 sub _items_payments {
4102 #get & print payments
4103 foreach ( $self->cust_bill_pay ) {
4105 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4108 'description' => "Payment received ".
4109 time2str("%x",$_->cust_pay->_date ),
4110 'amount' => sprintf("%.2f", $_->amount )
4118 =item call_details [ OPTION => VALUE ... ]
4120 Returns an array of CSV strings representing the call details for this invoice
4121 The only option available is the boolean prepend_billed_number
4126 my ($self, %opt) = @_;
4128 my $format_function = sub { shift };
4130 if ($opt{prepend_billed_number}) {
4131 $format_function = sub {
4135 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4140 my @details = map { $_->details( 'format_function' => $format_function,
4141 'escape_function' => sub{ return() },
4145 $self->cust_bill_pkg;
4146 my $header = $details[0];
4147 ( $header, grep { $_ ne $header } @details );
4157 =item process_reprint
4161 sub process_reprint {
4162 process_re_X('print', @_);
4165 =item process_reemail
4169 sub process_reemail {
4170 process_re_X('email', @_);
4178 process_re_X('fax', @_);
4186 process_re_X('ftp', @_);
4193 sub process_respool {
4194 process_re_X('spool', @_);
4197 use Storable qw(thaw);
4201 my( $method, $job ) = ( shift, shift );
4202 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4204 my $param = thaw(decode_base64(shift));
4205 warn Dumper($param) if $DEBUG;
4216 my($method, $job, %param ) = @_;
4218 warn "re_X $method for job $job with param:\n".
4219 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4222 #some false laziness w/search/cust_bill.html
4224 my $orderby = 'ORDER BY cust_bill._date';
4226 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4228 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4230 my @cust_bill = qsearch( {
4231 #'select' => "cust_bill.*",
4232 'table' => 'cust_bill',
4233 'addl_from' => $addl_from,
4235 'extra_sql' => $extra_sql,
4236 'order_by' => $orderby,
4240 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4242 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4245 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4246 foreach my $cust_bill ( @cust_bill ) {
4247 $cust_bill->$method();
4249 if ( $job ) { #progressbar foo
4251 if ( time - $min_sec > $last ) {
4252 my $error = $job->update_statustext(
4253 int( 100 * $num / scalar(@cust_bill) )
4255 die $error if $error;
4266 =head1 CLASS METHODS
4272 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4278 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4283 Returns an SQL fragment to retreive the net amount (charged minus credited).
4289 'charged - '. $class->credited_sql;
4294 Returns an SQL fragment to retreive the amount paid against this invoice.
4300 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4301 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4306 Returns an SQL fragment to retreive the amount credited against this invoice.
4312 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4313 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4316 =item search_sql_where HASHREF
4318 Class method which returns an SQL WHERE fragment to search for parameters
4319 specified in HASHREF. Valid parameters are
4325 List reference of start date, end date, as UNIX timestamps.
4335 List reference of charged limits (exclusive).
4339 List reference of charged limits (exclusive).
4343 flag, return open invoices only
4347 flag, return net invoices only
4351 =item newest_percust
4355 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4359 sub search_sql_where {
4360 my($class, $param) = @_;
4362 warn "$me search_sql_where called with params: \n".
4363 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4369 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4370 push @search, "cust_main.agentnum = $1";
4374 if ( $param->{_date} ) {
4375 my($beginning, $ending) = @{$param->{_date}};
4377 push @search, "cust_bill._date >= $beginning",
4378 "cust_bill._date < $ending";
4382 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4383 push @search, "cust_bill.invnum >= $1";
4385 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4386 push @search, "cust_bill.invnum <= $1";
4390 if ( $param->{charged} ) {
4391 my @charged = ref($param->{charged})
4392 ? @{ $param->{charged} }
4393 : ($param->{charged});
4395 push @search, map { s/^charged/cust_bill.charged/; $_; }
4399 my $owed_sql = FS::cust_bill->owed_sql;
4402 if ( $param->{owed} ) {
4403 my @owed = ref($param->{owed})
4404 ? @{ $param->{owed} }
4406 push @search, map { s/^owed/$owed_sql/; $_; }
4411 push @search, "0 != $owed_sql"
4412 if $param->{'open'};
4413 push @search, '0 != '. FS::cust_bill->net_sql
4417 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4418 if $param->{'days'};
4421 if ( $param->{'newest_percust'} ) {
4423 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4424 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4426 my @newest_where = map { my $x = $_;
4427 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4430 grep ! /^cust_main./, @search;
4431 my $newest_where = scalar(@newest_where)
4432 ? ' AND '. join(' AND ', @newest_where)
4436 push @search, "cust_bill._date = (
4437 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4438 WHERE newest_cust_bill.custnum = cust_bill.custnum
4444 #agent virtualization
4445 my $curuser = $FS::CurrentUser::CurrentUser;
4446 if ( $curuser->username eq 'fs_queue'
4447 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4449 my $newuser = qsearchs('access_user', {
4450 'username' => $username,
4454 $curuser = $newuser;
4456 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4459 push @search, $curuser->agentnums_sql;
4461 join(' AND ', @search );
4473 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4474 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base