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' : '',
2469 my $adjusttotal = 0;
2470 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2471 'subtotal' => 0, # adjusted below
2472 'summarized' => $summarypage ? 'Y' : '',
2475 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2476 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2477 my $late_sections = [];
2478 if ( $multisection ) {
2479 my ($extra_sections, $extra_lines) =
2480 $self->_items_extra_usage_sections($escape_function, $format)
2481 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2483 push @detail_items, @$extra_lines if $extra_lines;
2485 $self->_items_sections( $late_sections, # this could stand a refactor
2491 if ($conf->exists('svc_phone_sections')) {
2492 my ($phone_sections, $phone_lines) =
2493 $self->_items_svc_phone_sections($escape_function, $format);
2494 push @{$late_sections}, @$phone_sections;
2495 push @detail_items, @$phone_lines;
2498 push @sections, { 'description' => '', 'subtotal' => '' };
2501 unless ( $conf->exists('disable_previous_balance')
2502 || $conf->exists('previous_balance-summary_only')
2506 foreach my $line_item ( $self->_items_previous ) {
2509 ext_description => [],
2511 $detail->{'ref'} = $line_item->{'pkgnum'};
2512 $detail->{'quantity'} = 1;
2513 $detail->{'section'} = $previous_section;
2514 $detail->{'description'} = &$escape_function($line_item->{'description'});
2515 if ( exists $line_item->{'ext_description'} ) {
2516 @{$detail->{'ext_description'}} = map {
2517 &$escape_function($_);
2518 } @{$line_item->{'ext_description'}};
2520 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2521 $line_item->{'amount'};
2522 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2524 push @detail_items, $detail;
2525 push @buf, [ $detail->{'description'},
2526 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2532 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2533 push @buf, ['','-----------'];
2534 push @buf, [ 'Total Previous Balance',
2535 $money_char. sprintf("%10.2f", $pr_total) ];
2539 foreach my $section (@sections, @$late_sections) {
2541 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2542 if ( $invoice_data{finance_section} &&
2543 $section->{'description'} eq $invoice_data{finance_section} );
2545 $section->{'subtotal'} = $other_money_char.
2546 sprintf('%.2f', $section->{'subtotal'})
2549 # begin some normalization
2550 $section->{'amount'} = $section->{'subtotal'}
2554 if ( $section->{'description'} ) {
2555 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2561 $options{'section'} = $section if $multisection;
2562 $options{'format'} = $format;
2563 $options{'escape_function'} = $escape_function;
2564 $options{'format_function'} = sub { () } unless $unsquelched;
2565 $options{'unsquelched'} = $unsquelched;
2566 $options{'summary_page'} = $summarypage;
2568 foreach my $line_item ( $self->_items_pkg(%options) ) {
2570 ext_description => [],
2572 $detail->{'ref'} = $line_item->{'pkgnum'};
2573 $detail->{'quantity'} = $line_item->{'quantity'};
2574 $detail->{'section'} = $section;
2575 $detail->{'description'} = &$escape_function($line_item->{'description'});
2576 if ( exists $line_item->{'ext_description'} ) {
2577 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2579 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2580 $line_item->{'amount'};
2581 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2582 $line_item->{'unit_amount'};
2583 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2585 push @detail_items, $detail;
2586 push @buf, ( [ $detail->{'description'},
2587 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2589 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2593 if ( $section->{'description'} ) {
2594 push @buf, ( ['','-----------'],
2595 [ $section->{'description'}. ' sub-total',
2596 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2605 $invoice_data{current_less_finance} =
2606 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2608 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2609 unshift @sections, $previous_section if $pr_total;
2612 foreach my $tax ( $self->_items_tax ) {
2614 $taxtotal += $tax->{'amount'};
2616 my $description = &$escape_function( $tax->{'description'} );
2617 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2619 if ( $multisection ) {
2621 my $money = $old_latex ? '' : $money_char;
2622 push @detail_items, {
2623 ext_description => [],
2626 description => $description,
2627 amount => $money. $amount,
2629 section => $tax_section,
2634 push @total_items, {
2635 'total_item' => $description,
2636 'total_amount' => $other_money_char. $amount,
2641 push @buf,[ $description,
2642 $money_char. $amount,
2649 $total->{'total_item'} = 'Sub-total';
2650 $total->{'total_amount'} =
2651 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2653 if ( $multisection ) {
2654 $tax_section->{'subtotal'} = $other_money_char.
2655 sprintf('%.2f', $taxtotal);
2656 $tax_section->{'pretotal'} = 'New charges sub-total '.
2657 $total->{'total_amount'};
2658 push @sections, $tax_section if $taxtotal;
2660 unshift @total_items, $total;
2663 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2665 push @buf,['','-----------'];
2666 push @buf,[( $conf->exists('disable_previous_balance')
2668 : 'Total New Charges'
2670 $money_char. sprintf("%10.2f",$self->charged) ];
2675 $total->{'total_item'} = &$embolden_function('Total');
2676 $total->{'total_amount'} =
2677 &$embolden_function(
2680 $self->charged + ( $conf->exists('disable_previous_balance')
2686 if ( $multisection ) {
2687 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2688 sprintf('%.2f', $self->charged );
2690 push @total_items, $total;
2692 push @buf,['','-----------'];
2693 push @buf,['Total Charges',
2695 sprintf( '%10.2f', $self->charged +
2696 ( $conf->exists('disable_previous_balance')
2705 unless ( $conf->exists('disable_previous_balance') ) {
2706 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2709 my $credittotal = 0;
2710 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2713 $total->{'total_item'} = &$escape_function($credit->{'description'});
2714 $credittotal += $credit->{'amount'};
2715 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2716 $adjusttotal += $credit->{'amount'};
2717 if ( $multisection ) {
2718 my $money = $old_latex ? '' : $money_char;
2719 push @detail_items, {
2720 ext_description => [],
2723 description => &$escape_function($credit->{'description'}),
2724 amount => $money. $credit->{'amount'},
2726 section => $adjust_section,
2729 push @total_items, $total;
2733 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2736 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2737 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2741 my $paymenttotal = 0;
2742 foreach my $payment ( $self->_items_payments ) {
2744 $total->{'total_item'} = &$escape_function($payment->{'description'});
2745 $paymenttotal += $payment->{'amount'};
2746 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2747 $adjusttotal += $payment->{'amount'};
2748 if ( $multisection ) {
2749 my $money = $old_latex ? '' : $money_char;
2750 push @detail_items, {
2751 ext_description => [],
2754 description => &$escape_function($payment->{'description'}),
2755 amount => $money. $payment->{'amount'},
2757 section => $adjust_section,
2760 push @total_items, $total;
2762 push @buf, [ $payment->{'description'},
2763 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2766 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2768 if ( $multisection ) {
2769 $adjust_section->{'subtotal'} = $other_money_char.
2770 sprintf('%.2f', $adjusttotal);
2771 push @sections, $adjust_section;
2776 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2777 $total->{'total_amount'} =
2778 &$embolden_function(
2779 $other_money_char. sprintf('%.2f', $summarypage
2781 $self->billing_balance
2782 : $self->owed + $pr_total
2785 if ( $multisection ) {
2786 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2787 $total->{'total_amount'};
2789 push @total_items, $total;
2791 push @buf,['','-----------'];
2792 push @buf,[$self->balance_due_msg, $money_char.
2793 sprintf("%10.2f", $balance_due ) ];
2797 if ( $multisection ) {
2798 push @sections, @$late_sections
2802 my @includelist = ();
2803 push @includelist, 'summary' if $summarypage;
2804 foreach my $include ( @includelist ) {
2806 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2809 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2811 @inc_src = $conf->config($inc_file, $agentnum);
2815 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2817 my $convert_map = $convert_maps{$format}{$include};
2819 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2820 s/--\@\]/$delimiters{$format}[1]/g;
2823 &$convert_map( $conf->config($inc_file, $agentnum) );
2827 my $inc_tt = new Text::Template (
2829 SOURCE => [ map "$_\n", @inc_src ],
2830 DELIMITERS => $delimiters{$format},
2831 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2833 unless ( $inc_tt->compile() ) {
2834 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2835 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2839 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2841 $invoice_data{$include} =~ s/\n+$//
2842 if ($format eq 'latex');
2847 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2848 /invoice_lines\((\d*)\)/;
2849 $invoice_lines += $1 || scalar(@buf);
2852 die "no invoice_lines() functions in template?"
2853 if ( $format eq 'template' && !$wasfunc );
2855 if ($format eq 'template') {
2857 if ( $invoice_lines ) {
2858 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2859 $invoice_data{'total_pages'}++
2860 if scalar(@buf) % $invoice_lines;
2863 #setup subroutine for the template
2864 sub FS::cust_bill::_template::invoice_lines {
2865 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2867 scalar(@FS::cust_bill::_template::buf)
2868 ? shift @FS::cust_bill::_template::buf
2877 push @collect, split("\n",
2878 $text_template->fill_in( HASH => \%invoice_data,
2879 PACKAGE => 'FS::cust_bill::_template'
2882 $FS::cust_bill::_template::page++;
2884 map "$_\n", @collect;
2886 warn "filling in template for invoice ". $self->invnum. "\n"
2888 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2891 $text_template->fill_in(HASH => \%invoice_data);
2895 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2897 Returns an postscript invoice, as a scalar.
2899 Options can be passed as a hashref (recommended) or as a list of time, template
2900 and then any key/value pairs for any other options.
2902 I<time> an optional value used to control the printing of overdue messages. The
2903 default is now. It isn't the date of the invoice; that's the `_date' field.
2904 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2905 L<Time::Local> and L<Date::Parse> for conversion functions.
2907 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2914 my ($file, $lfile) = $self->print_latex(@_);
2915 my $ps = generate_ps($file);
2921 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2923 Returns an PDF invoice, as a scalar.
2925 Options can be passed as a hashref (recommended) or as a list of time, template
2926 and then any key/value pairs for any other options.
2928 I<time> an optional value used to control the printing of overdue messages. The
2929 default is now. It isn't the date of the invoice; that's the `_date' field.
2930 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2931 L<Time::Local> and L<Date::Parse> for conversion functions.
2933 I<template>, if specified, is the name of a suffix for alternate invoices.
2935 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2942 my ($file, $lfile) = $self->print_latex(@_);
2943 my $pdf = generate_pdf($file);
2949 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2951 Returns an HTML invoice, as a scalar.
2953 I<time> an optional value used to control the printing of overdue messages. The
2954 default is now. It isn't the date of the invoice; that's the `_date' field.
2955 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2956 L<Time::Local> and L<Date::Parse> for conversion functions.
2958 I<template>, if specified, is the name of a suffix for alternate invoices.
2960 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2962 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2963 when emailing the invoice as part of a multipart/related MIME email.
2971 %params = %{ shift() };
2973 $params{'time'} = shift;
2974 $params{'template'} = shift;
2975 $params{'cid'} = shift;
2978 $params{'format'} = 'html';
2980 $self->print_generic( %params );
2983 # quick subroutine for print_latex
2985 # There are ten characters that LaTeX treats as special characters, which
2986 # means that they do not simply typeset themselves:
2987 # # $ % & ~ _ ^ \ { }
2989 # TeX ignores blanks following an escaped character; if you want a blank (as
2990 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2994 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2995 $value =~ s/([<>])/\$$1\$/g;
2999 #utility methods for print_*
3001 sub _translate_old_latex_format {
3002 warn "_translate_old_latex_format called\n"
3009 if ( $line =~ /^%%Detail\s*$/ ) {
3011 push @template, q![@--!,
3012 q! foreach my $_tr_line (@detail_items) {!,
3013 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3014 q! $_tr_line->{'description'} .= !,
3015 q! "\\tabularnewline\n~~".!,
3016 q! join( "\\tabularnewline\n~~",!,
3017 q! @{$_tr_line->{'ext_description'}}!,
3021 while ( ( my $line_item_line = shift )
3022 !~ /^%%EndDetail\s*$/ ) {
3023 $line_item_line =~ s/'/\\'/g; # nice LTS
3024 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3025 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3026 push @template, " \$OUT .= '$line_item_line';";
3029 push @template, '}',
3032 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3034 push @template, '[@--',
3035 ' foreach my $_tr_line (@total_items) {';
3037 while ( ( my $total_item_line = shift )
3038 !~ /^%%EndTotalDetails\s*$/ ) {
3039 $total_item_line =~ s/'/\\'/g; # nice LTS
3040 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3041 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3042 push @template, " \$OUT .= '$total_item_line';";
3045 push @template, '}',
3049 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3050 push @template, $line;
3056 warn "$_\n" foreach @template;
3065 #check for an invoice-specific override
3066 return $self->invoice_terms if $self->invoice_terms;
3068 #check for a customer- specific override
3069 my $cust_main = $self->cust_main;
3070 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3072 #use configured default
3073 $conf->config('invoice_default_terms') || '';
3079 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3080 $duedate = $self->_date() + ( $1 * 86400 );
3087 $self->due_date ? time2str(shift, $self->due_date) : '';
3090 sub balance_due_msg {
3092 my $msg = 'Balance Due';
3093 return $msg unless $self->terms;
3094 if ( $self->due_date ) {
3095 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3096 } elsif ( $self->terms ) {
3097 $msg .= ' - '. $self->terms;
3102 sub balance_due_date {
3105 if ( $conf->exists('invoice_default_terms')
3106 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3107 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3112 =item invnum_date_pretty
3114 Returns a string with the invoice number and date, for example:
3115 "Invoice #54 (3/20/2008)"
3119 sub invnum_date_pretty {
3121 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3126 Returns a string with the date, for example: "3/20/2008"
3132 time2str('%x', $self->_date);
3135 use vars qw(%pkg_category_cache);
3136 sub _items_sections {
3139 my $summarypage = shift;
3141 my $extra_sections = shift;
3145 my %late_subtotal = ();
3148 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3151 my $usage = $cust_bill_pkg->usage;
3153 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3154 next if ( $display->summary && $summarypage );
3156 my $section = $display->section;
3157 my $type = $display->type;
3159 $not_tax{$section} = 1
3160 unless $cust_bill_pkg->pkgnum == 0;
3162 if ( $display->post_total && !$summarypage ) {
3163 if (! $type || $type eq 'S') {
3164 $late_subtotal{$section} += $cust_bill_pkg->setup
3165 if $cust_bill_pkg->setup != 0;
3169 $late_subtotal{$section} += $cust_bill_pkg->recur
3170 if $cust_bill_pkg->recur != 0;
3173 if ($type && $type eq 'R') {
3174 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3175 if $cust_bill_pkg->recur != 0;
3178 if ($type && $type eq 'U') {
3179 $late_subtotal{$section} += $usage;
3184 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3186 if (! $type || $type eq 'S') {
3187 $subtotal{$section} += $cust_bill_pkg->setup
3188 if $cust_bill_pkg->setup != 0;
3192 $subtotal{$section} += $cust_bill_pkg->recur
3193 if $cust_bill_pkg->recur != 0;
3196 if ($type && $type eq 'R') {
3197 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3198 if $cust_bill_pkg->recur != 0;
3201 if ($type && $type eq 'U') {
3202 $subtotal{$section} += $usage;
3211 %pkg_category_cache = ();
3213 push @$late, map { { 'description' => &{$escape}($_),
3214 'subtotal' => $late_subtotal{$_},
3216 'sort_weight' => ( _pkg_category($_)
3217 ? _pkg_category($_)->weight
3220 ((_pkg_category($_) && _pkg_category($_)->condense)
3221 ? $self->_condense_section($format)
3225 sort _sectionsort keys %late_subtotal;
3228 if ( $summarypage ) {
3229 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3230 map { $_->categoryname } qsearch('pkg_category', {});
3232 @sections = keys %subtotal;
3235 my @early = map { { 'description' => &{$escape}($_),
3236 'subtotal' => $subtotal{$_},
3237 'summarized' => $not_tax{$_} ? '' : 'Y',
3238 'tax_section' => $not_tax{$_} ? '' : 'Y',
3239 'sort_weight' => ( _pkg_category($_)
3240 ? _pkg_category($_)->weight
3243 ((_pkg_category($_) && _pkg_category($_)->condense)
3244 ? $self->_condense_section($format)
3249 push @early, @$extra_sections if $extra_sections;
3251 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3255 #helper subs for above
3258 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3262 my $categoryname = shift;
3263 $pkg_category_cache{$categoryname} ||=
3264 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3267 my %condensed_format = (
3268 'label' => [ qw( Description Qty Amount ) ],
3270 sub { shift->{description} },
3271 sub { shift->{quantity} },
3272 sub { shift->{amount} },
3274 'align' => [ qw( l r r ) ],
3275 'span' => [ qw( 5 1 1 ) ], # unitprices?
3276 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3279 sub _condense_section {
3280 my ( $self, $format ) = ( shift, shift );
3282 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3283 qw( description_generator
3286 total_line_generator
3291 sub _condensed_generator_defaults {
3292 my ( $self, $format ) = ( shift, shift );
3293 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3302 sub _condensed_header_generator {
3303 my ( $self, $format ) = ( shift, shift );
3305 my ( $f, $prefix, $suffix, $separator, $column ) =
3306 _condensed_generator_defaults($format);
3308 if ($format eq 'latex') {
3309 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3310 $suffix = "\\\\\n\\hline";
3313 sub { my ($d,$a,$s,$w) = @_;
3314 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3316 } elsif ( $format eq 'html' ) {
3317 $prefix = '<th></th>';
3321 sub { my ($d,$a,$s,$w) = @_;
3322 return qq!<th align="$html_align{$a}">$d</th>!;
3330 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3332 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3335 $prefix. join($separator, @result). $suffix;
3340 sub _condensed_description_generator {
3341 my ( $self, $format ) = ( shift, shift );
3343 my ( $f, $prefix, $suffix, $separator, $column ) =
3344 _condensed_generator_defaults($format);
3346 if ($format eq 'latex') {
3347 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3349 $separator = " & \n";
3351 sub { my ($d,$a,$s,$w) = @_;
3352 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3354 }elsif ( $format eq 'html' ) {
3355 $prefix = '"><td align="center"></td>';
3359 sub { my ($d,$a,$s,$w) = @_;
3360 return qq!<td align="$html_align{$a}">$d</td>!;
3368 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3369 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3370 map { $f->{$_}->[$i] } qw(align span width)
3374 $prefix. join( $separator, @result ). $suffix;
3379 sub _condensed_total_generator {
3380 my ( $self, $format ) = ( shift, shift );
3382 my ( $f, $prefix, $suffix, $separator, $column ) =
3383 _condensed_generator_defaults($format);
3386 if ($format eq 'latex') {
3389 $separator = " & \n";
3391 sub { my ($d,$a,$s,$w) = @_;
3392 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3394 }elsif ( $format eq 'html' ) {
3398 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3400 sub { my ($d,$a,$s,$w) = @_;
3401 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3410 # my $r = &{$f->{fields}->[$i]}(@args);
3411 # $r .= ' Total' unless $i;
3413 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3415 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3416 map { $f->{$_}->[$i] } qw(align span width)
3420 $prefix. join( $separator, @result ). $suffix;
3425 =item total_line_generator FORMAT
3427 Returns a coderef used for generation of invoice total line items for this
3428 usage_class. FORMAT is either html or latex
3432 # should not be used: will have issues with hash element names (description vs
3433 # total_item and amount vs total_amount -- another array of functions?
3435 sub _condensed_total_line_generator {
3436 my ( $self, $format ) = ( shift, shift );
3438 my ( $f, $prefix, $suffix, $separator, $column ) =
3439 _condensed_generator_defaults($format);
3442 if ($format eq 'latex') {
3445 $separator = " & \n";
3447 sub { my ($d,$a,$s,$w) = @_;
3448 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3450 }elsif ( $format eq 'html' ) {
3454 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3456 sub { my ($d,$a,$s,$w) = @_;
3457 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3466 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3468 &{$column}( &{$f->{fields}->[$i]}(@args),
3469 map { $f->{$_}->[$i] } qw(align span width)
3473 $prefix. join( $separator, @result ). $suffix;
3478 #sub _items_extra_usage_sections {
3480 # my $escape = shift;
3482 # my %sections = ();
3484 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3485 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3487 # next unless $cust_bill_pkg->pkgnum > 0;
3489 # foreach my $section ( keys %usage_class ) {
3491 # my $usage = $cust_bill_pkg->usage($section);
3493 # next unless $usage && $usage > 0;
3495 # $sections{$section} ||= 0;
3496 # $sections{$section} += $usage;
3502 # map { { 'description' => &{$escape}($_),
3503 # 'subtotal' => $sections{$_},
3504 # 'summarized' => '',
3505 # 'tax_section' => '',
3508 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3512 sub _items_extra_usage_sections {
3521 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3522 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3523 next unless $cust_bill_pkg->pkgnum > 0;
3525 foreach my $classnum ( keys %usage_class ) {
3526 my $section = $usage_class{$classnum}->classname;
3527 $classnums{$section} = $classnum;
3529 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3530 my $amount = $detail->amount;
3531 next unless $amount && $amount > 0;
3533 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3534 $sections{$section}{amount} += $amount; #subtotal
3535 $sections{$section}{calls}++;
3536 $sections{$section}{duration} += $detail->duration;
3538 my $desc = $detail->regionname;
3539 my $description = $desc;
3540 $description = substr($desc, 0, 50). '...'
3541 if $format eq 'latex' && length($desc) > 50;
3543 $lines{$section}{$desc} ||= {
3544 description => &{$escape}($description),
3545 #pkgpart => $part_pkg->pkgpart,
3546 pkgnum => $cust_bill_pkg->pkgnum,
3551 #unit_amount => $cust_bill_pkg->unitrecur,
3552 quantity => $cust_bill_pkg->quantity,
3553 product_code => 'N/A',
3554 ext_description => [],
3557 $lines{$section}{$desc}{amount} += $amount;
3558 $lines{$section}{$desc}{calls}++;
3559 $lines{$section}{$desc}{duration} += $detail->duration;
3565 my %sectionmap = ();
3566 foreach (keys %sections) {
3567 my $usage_class = $usage_class{$classnums{$_}};
3568 $sectionmap{$_} = { 'description' => &{$escape}($_),
3569 'amount' => $sections{$_}{amount}, #subtotal
3570 'calls' => $sections{$_}{calls},
3571 'duration' => $sections{$_}{duration},
3573 'tax_section' => '',
3574 'sort_weight' => $usage_class->weight,
3575 ( $usage_class->format
3576 ? ( map { $_ => $usage_class->$_($format) }
3577 qw( description_generator header_generator total_generator total_line_generator )
3584 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3588 foreach my $section ( keys %lines ) {
3589 foreach my $line ( keys %{$lines{$section}} ) {
3590 my $l = $lines{$section}{$line};
3591 $l->{section} = $sectionmap{$section};
3592 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3593 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3598 return(\@sections, \@lines);
3602 sub _items_svc_phone_sections {
3611 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3613 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3614 next unless $cust_bill_pkg->pkgnum > 0;
3616 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3618 my $phonenum = $detail->phonenum;
3619 next unless $phonenum;
3621 my $amount = $detail->amount;
3622 next unless $amount && $amount > 0;
3624 $sections{$phonenum} ||= { 'amount' => 0,
3627 'sort_weight' => -1,
3628 'phonenum' => $phonenum,
3630 $sections{$phonenum}{amount} += $amount; #subtotal
3631 $sections{$phonenum}{calls}++;
3632 $sections{$phonenum}{duration} += $detail->duration;
3634 my $desc = $detail->regionname;
3635 my $description = $desc;
3636 $description = substr($desc, 0, 50). '...'
3637 if $format eq 'latex' && length($desc) > 50;
3639 $lines{$phonenum}{$desc} ||= {
3640 description => &{$escape}($description),
3641 #pkgpart => $part_pkg->pkgpart,
3649 product_code => 'N/A',
3650 ext_description => [],
3653 $lines{$phonenum}{$desc}{amount} += $amount;
3654 $lines{$phonenum}{$desc}{calls}++;
3655 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3657 my $line = $usage_class{$detail->classnum}->classname;
3658 $sections{"$phonenum $line"} ||=
3662 'sort_weight' => $usage_class{$detail->classnum}->weight,
3663 'phonenum' => $phonenum,
3665 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3666 $sections{"$phonenum $line"}{calls}++;
3667 $sections{"$phonenum $line"}{duration} += $detail->duration;
3669 $lines{"$phonenum $line"}{$desc} ||= {
3670 description => &{$escape}($description),
3671 #pkgpart => $part_pkg->pkgpart,
3679 product_code => 'N/A',
3680 ext_description => [],
3683 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3684 $lines{"$phonenum $line"}{$desc}{calls}++;
3685 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3686 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3687 $detail->formatted('format' => $format);
3692 my %sectionmap = ();
3693 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3694 my $minimal = new FS::usage_class { format => 'minimal' }; #bleh
3695 foreach ( keys %sections ) {
3696 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3697 my $usage_class = $summary ? $simple : $minimal;
3698 my $ending = $summary ? ' usage charges' : '';
3699 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3700 'amount' => $sections{$_}{amount}, #subtotal
3701 'calls' => $sections{$_}{calls},
3702 'duration' => $sections{$_}{duration},
3704 'tax_section' => '',
3705 'phonenum' => $sections{$_}{phonenum},
3706 'sort_weight' => $sections{$_}{sort_weight},
3708 ( map { $_ => $usage_class->$_($format) }
3709 qw( description_generator
3712 total_line_generator
3719 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3720 $a->{sort_weight} <=> $b->{sort_weight}
3725 foreach my $section ( keys %lines ) {
3726 foreach my $line ( keys %{$lines{$section}} ) {
3727 my $l = $lines{$section}{$line};
3728 $l->{section} = $sectionmap{$section};
3729 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3730 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3735 return(\@sections, \@lines);
3742 #my @display = scalar(@_)
3744 # : qw( _items_previous _items_pkg );
3745 # #: qw( _items_pkg );
3746 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3747 my @display = qw( _items_previous _items_pkg );
3750 foreach my $display ( @display ) {
3751 push @b, $self->$display(@_);
3756 sub _items_previous {
3758 my $cust_main = $self->cust_main;
3759 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3761 foreach ( @pr_cust_bill ) {
3763 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3764 ' ('. time2str('%x',$_->_date). ')',
3765 #'pkgpart' => 'N/A',
3767 'amount' => sprintf("%.2f", $_->owed),
3773 # 'description' => 'Previous Balance',
3774 # #'pkgpart' => 'N/A',
3775 # 'pkgnum' => 'N/A',
3776 # 'amount' => sprintf("%10.2f", $pr_total ),
3777 # 'ext_description' => [ map {
3778 # "Invoice ". $_->invnum.
3779 # " (". time2str("%x",$_->_date). ") ".
3780 # sprintf("%10.2f", $_->owed)
3781 # } @pr_cust_bill ],
3789 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3790 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3791 if ($options{section} && $options{section}->{condensed}) {
3793 local $Storable::canonical = 1;
3794 foreach ( @items ) {
3796 delete $item->{ref};
3797 delete $item->{ext_description};
3798 my $key = freeze($item);
3799 $itemshash{$key} ||= 0;
3800 $itemshash{$key} ++; # += $item->{quantity};
3802 @items = sort { $a->{description} cmp $b->{description} }
3803 map { my $i = thaw($_);
3804 $i->{quantity} = $itemshash{$_};
3806 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3815 return 0 unless $a cmp $b;
3816 return -1 if $b eq 'Tax';
3817 return 1 if $a eq 'Tax';
3818 return -1 if $b eq 'Other surcharges';
3819 return 1 if $a eq 'Other surcharges';
3825 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3826 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3829 sub _items_cust_bill_pkg {
3831 my $cust_bill_pkg = shift;
3834 my $format = $opt{format} || '';
3835 my $escape_function = $opt{escape_function} || sub { shift };
3836 my $format_function = $opt{format_function} || '';
3837 my $unsquelched = $opt{unsquelched} || '';
3838 my $section = $opt{section}->{description} if $opt{section};
3839 my $summary_page = $opt{summary_page} || '';
3842 my ($s, $r, $u) = ( undef, undef, undef );
3843 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3846 foreach ( $s, $r, $u ) {
3847 if ( $_ && !$cust_bill_pkg->hidden ) {
3848 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3849 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3855 foreach my $display ( grep { defined($section)
3856 ? $_->section eq $section
3859 grep { !$_->summary || !$summary_page }
3860 $cust_bill_pkg->cust_bill_pkg_display
3864 my $type = $display->type;
3866 my $desc = $cust_bill_pkg->desc;
3867 $desc = substr($desc, 0, 50). '...'
3868 if $format eq 'latex' && length($desc) > 50;
3870 my %details_opt = ( 'format' => $format,
3871 'escape_function' => $escape_function,
3872 'format_function' => $format_function,
3875 if ( $cust_bill_pkg->pkgnum > 0 ) {
3877 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3879 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3881 my $description = $desc;
3882 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3885 push @d, map &{$escape_function}($_),
3886 $cust_pkg->h_labels_short($self->_date)
3887 unless $cust_pkg->part_pkg->hide_svc_detail
3888 || $cust_bill_pkg->hidden;
3889 push @d, $cust_bill_pkg->details(%details_opt)
3890 if $cust_bill_pkg->recur == 0;
3892 if ( $cust_bill_pkg->hidden ) {
3893 $s->{amount} += $cust_bill_pkg->setup;
3894 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3895 push @{ $s->{ext_description} }, @d;
3898 description => $description,
3899 #pkgpart => $part_pkg->pkgpart,
3900 pkgnum => $cust_bill_pkg->pkgnum,
3901 amount => $cust_bill_pkg->setup,
3902 unit_amount => $cust_bill_pkg->unitsetup,
3903 quantity => $cust_bill_pkg->quantity,
3904 ext_description => \@d,
3910 if ( $cust_bill_pkg->recur != 0 &&
3911 ( !$type || $type eq 'R' || $type eq 'U' )
3915 my $is_summary = $display->summary;
3916 my $description = ($is_summary && $type && $type eq 'U')
3917 ? "Usage charges" : $desc;
3919 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3920 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3921 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3926 #at least until cust_bill_pkg has "past" ranges in addition to
3927 #the "future" sdate/edate ones... see #3032
3928 my @dates = ( $self->_date );
3929 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3930 push @dates, $prev->sdate if $prev;
3932 push @d, map &{$escape_function}($_),
3933 $cust_pkg->h_labels_short(@dates)
3934 #$cust_bill_pkg->edate,
3935 #$cust_bill_pkg->sdate)
3936 unless $cust_pkg->part_pkg->hide_svc_detail
3937 || $cust_bill_pkg->itemdesc
3938 || $cust_bill_pkg->hidden
3939 || $is_summary && $type && $type eq 'U';
3941 push @d, $cust_bill_pkg->details(%details_opt)
3942 unless ($is_summary || $type && $type eq 'R');
3946 $amount = $cust_bill_pkg->recur;
3947 }elsif($type eq 'R') {
3948 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3949 }elsif($type eq 'U') {
3950 $amount = $cust_bill_pkg->usage;
3953 if ( !$type || $type eq 'R' ) {
3955 if ( $cust_bill_pkg->hidden ) {
3956 $r->{amount} += $amount;
3957 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3958 push @{ $r->{ext_description} }, @d;
3961 description => $description,
3962 #pkgpart => $part_pkg->pkgpart,
3963 pkgnum => $cust_bill_pkg->pkgnum,
3965 unit_amount => $cust_bill_pkg->unitrecur,
3966 quantity => $cust_bill_pkg->quantity,
3967 ext_description => \@d,
3971 } elsif ( $amount ) { # && $type eq 'U'
3973 if ( $cust_bill_pkg->hidden ) {
3974 $u->{amount} += $amount;
3975 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3976 push @{ $u->{ext_description} }, @d;
3979 description => $description,
3980 #pkgpart => $part_pkg->pkgpart,
3981 pkgnum => $cust_bill_pkg->pkgnum,
3983 unit_amount => $cust_bill_pkg->unitrecur,
3984 quantity => $cust_bill_pkg->quantity,
3985 ext_description => \@d,
3991 } # recurring or usage with recurring charge
3993 } else { #pkgnum tax or one-shot line item (??)
3995 if ( $cust_bill_pkg->setup != 0 ) {
3997 'description' => $desc,
3998 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4001 if ( $cust_bill_pkg->recur != 0 ) {
4003 'description' => "$desc (".
4004 time2str("%x", $cust_bill_pkg->sdate). ' - '.
4005 time2str("%x", $cust_bill_pkg->edate). ')',
4006 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4016 foreach ( $s, $r, $u ) {
4018 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4019 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4028 sub _items_credits {
4029 my( $self, %opt ) = @_;
4030 my $trim_len = $opt{'trim_len'} || 60;
4034 foreach ( $self->cust_credited ) {
4036 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4038 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4039 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4040 $reason = " ($reason) " if $reason;
4043 #'description' => 'Credit ref\#'. $_->crednum.
4044 # " (". time2str("%x",$_->cust_credit->_date) .")".
4046 'description' => 'Credit applied '.
4047 time2str("%x",$_->cust_credit->_date). $reason,
4048 'amount' => sprintf("%.2f",$_->amount),
4056 sub _items_payments {
4060 #get & print payments
4061 foreach ( $self->cust_bill_pay ) {
4063 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4066 'description' => "Payment received ".
4067 time2str("%x",$_->cust_pay->_date ),
4068 'amount' => sprintf("%.2f", $_->amount )
4076 =item call_details [ OPTION => VALUE ... ]
4078 Returns an array of CSV strings representing the call details for this invoice
4079 The only option available is the boolean prepend_billed_number
4084 my ($self, %opt) = @_;
4086 my $format_function = sub { shift };
4088 if ($opt{prepend_billed_number}) {
4089 $format_function = sub {
4093 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4098 my @details = map { $_->details( 'format_function' => $format_function,
4099 'escape_function' => sub{ return() },
4103 $self->cust_bill_pkg;
4104 my $header = $details[0];
4105 ( $header, grep { $_ ne $header } @details );
4115 =item process_reprint
4119 sub process_reprint {
4120 process_re_X('print', @_);
4123 =item process_reemail
4127 sub process_reemail {
4128 process_re_X('email', @_);
4136 process_re_X('fax', @_);
4144 process_re_X('ftp', @_);
4151 sub process_respool {
4152 process_re_X('spool', @_);
4155 use Storable qw(thaw);
4159 my( $method, $job ) = ( shift, shift );
4160 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4162 my $param = thaw(decode_base64(shift));
4163 warn Dumper($param) if $DEBUG;
4174 my($method, $job, %param ) = @_;
4176 warn "re_X $method for job $job with param:\n".
4177 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4180 #some false laziness w/search/cust_bill.html
4182 my $orderby = 'ORDER BY cust_bill._date';
4184 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
4186 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4188 my @cust_bill = qsearch( {
4189 #'select' => "cust_bill.*",
4190 'table' => 'cust_bill',
4191 'addl_from' => $addl_from,
4193 'extra_sql' => $extra_sql,
4194 'order_by' => $orderby,
4198 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4200 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4203 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4204 foreach my $cust_bill ( @cust_bill ) {
4205 $cust_bill->$method();
4207 if ( $job ) { #progressbar foo
4209 if ( time - $min_sec > $last ) {
4210 my $error = $job->update_statustext(
4211 int( 100 * $num / scalar(@cust_bill) )
4213 die $error if $error;
4224 =head1 CLASS METHODS
4230 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4236 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4241 Returns an SQL fragment to retreive the net amount (charged minus credited).
4247 'charged - '. $class->credited_sql;
4252 Returns an SQL fragment to retreive the amount paid against this invoice.
4258 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4259 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4264 Returns an SQL fragment to retreive the amount credited against this invoice.
4270 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4271 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4274 =item search_sql HASHREF
4276 Class method which returns an SQL WHERE fragment to search for parameters
4277 specified in HASHREF. Valid parameters are
4283 List reference of start date, end date, as UNIX timestamps.
4293 List reference of charged limits (exclusive).
4297 List reference of charged limits (exclusive).
4301 flag, return open invoices only
4305 flag, return net invoices only
4309 =item newest_percust
4313 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4318 my($class, $param) = @_;
4320 warn "$me search_sql called with params: \n".
4321 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4327 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4328 push @search, "cust_main.agentnum = $1";
4332 if ( $param->{_date} ) {
4333 my($beginning, $ending) = @{$param->{_date}};
4335 push @search, "cust_bill._date >= $beginning",
4336 "cust_bill._date < $ending";
4340 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4341 push @search, "cust_bill.invnum >= $1";
4343 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4344 push @search, "cust_bill.invnum <= $1";
4348 if ( $param->{charged} ) {
4349 my @charged = ref($param->{charged})
4350 ? @{ $param->{charged} }
4351 : ($param->{charged});
4353 push @search, map { s/^charged/cust_bill.charged/; $_; }
4357 my $owed_sql = FS::cust_bill->owed_sql;
4360 if ( $param->{owed} ) {
4361 my @owed = ref($param->{owed})
4362 ? @{ $param->{owed} }
4364 push @search, map { s/^owed/$owed_sql/; $_; }
4369 push @search, "0 != $owed_sql"
4370 if $param->{'open'};
4371 push @search, '0 != '. FS::cust_bill->net_sql
4375 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4376 if $param->{'days'};
4379 if ( $param->{'newest_percust'} ) {
4381 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4382 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4384 my @newest_where = map { my $x = $_;
4385 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4388 grep ! /^cust_main./, @search;
4389 my $newest_where = scalar(@newest_where)
4390 ? ' AND '. join(' AND ', @newest_where)
4394 push @search, "cust_bill._date = (
4395 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4396 WHERE newest_cust_bill.custnum = cust_bill.custnum
4402 #agent virtualization
4403 my $curuser = $FS::CurrentUser::CurrentUser;
4404 if ( $curuser->username eq 'fs_queue'
4405 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4407 my $newuser = qsearchs('access_user', {
4408 'username' => $username,
4412 $curuser = $newuser;
4414 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4417 push @search, $curuser->agentnums_sql;
4419 join(' AND ', @search );
4431 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4432 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base