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($_)->weight,
3217 (_pkg_category($_)->condense
3218 ? $self->_condense_section($format)
3222 sort _sectionsort keys %late_subtotal;
3225 if ( $summarypage ) {
3226 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3227 map { $_->categoryname } qsearch('pkg_category', {});
3229 @sections = keys %subtotal;
3232 my @early = map { { 'description' => &{$escape}($_),
3233 'subtotal' => $subtotal{$_},
3234 'summarized' => $not_tax{$_} ? '' : 'Y',
3235 'tax_section' => $not_tax{$_} ? '' : 'Y',
3236 'sort_weight' => _pkg_category($_)->weight,
3237 (_pkg_category($_)->condense
3238 ? $self->_condense_section($format)
3243 push @early, @$extra_sections if $extra_sections;
3245 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3249 #helper subs for above
3252 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3256 my $categoryname = shift;
3257 $pkg_category_cache{$categoryname} ||=
3258 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3261 my %condensed_format = (
3262 'label' => [ qw( Description Qty Amount ) ],
3264 sub { shift->{description} },
3265 sub { shift->{quantity} },
3266 sub { shift->{amount} },
3268 'align' => [ qw( l r r ) ],
3269 'span' => [ qw( 5 1 1 ) ], # unitprices?
3270 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3273 sub _condense_section {
3274 my ( $self, $format ) = ( shift, shift );
3276 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3277 qw( description_generator
3280 total_line_generator
3285 sub _condensed_generator_defaults {
3286 my ( $self, $format ) = ( shift, shift );
3287 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3296 sub _condensed_header_generator {
3297 my ( $self, $format ) = ( shift, shift );
3299 my ( $f, $prefix, $suffix, $separator, $column ) =
3300 _condensed_generator_defaults($format);
3302 if ($format eq 'latex') {
3303 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3304 $suffix = "\\\\\n\\hline";
3307 sub { my ($d,$a,$s,$w) = @_;
3308 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3310 } elsif ( $format eq 'html' ) {
3311 $prefix = '<th></th>';
3315 sub { my ($d,$a,$s,$w) = @_;
3316 return qq!<th align="$html_align{$a}">$d</th>!;
3324 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3326 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3329 $prefix. join($separator, @result). $suffix;
3334 sub _condensed_description_generator {
3335 my ( $self, $format ) = ( shift, shift );
3337 my ( $f, $prefix, $suffix, $separator, $column ) =
3338 _condensed_generator_defaults($format);
3340 if ($format eq 'latex') {
3341 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3343 $separator = " & \n";
3345 sub { my ($d,$a,$s,$w) = @_;
3346 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3348 }elsif ( $format eq 'html' ) {
3349 $prefix = '"><td align="center"></td>';
3353 sub { my ($d,$a,$s,$w) = @_;
3354 return qq!<td align="$html_align{$a}">$d</td>!;
3362 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3363 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3364 map { $f->{$_}->[$i] } qw(align span width)
3368 $prefix. join( $separator, @result ). $suffix;
3373 sub _condensed_total_generator {
3374 my ( $self, $format ) = ( shift, shift );
3376 my ( $f, $prefix, $suffix, $separator, $column ) =
3377 _condensed_generator_defaults($format);
3380 if ($format eq 'latex') {
3383 $separator = " & \n";
3385 sub { my ($d,$a,$s,$w) = @_;
3386 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3388 }elsif ( $format eq 'html' ) {
3392 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3394 sub { my ($d,$a,$s,$w) = @_;
3395 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3404 # my $r = &{$f->{fields}->[$i]}(@args);
3405 # $r .= ' Total' unless $i;
3407 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3409 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3410 map { $f->{$_}->[$i] } qw(align span width)
3414 $prefix. join( $separator, @result ). $suffix;
3419 =item total_line_generator FORMAT
3421 Returns a coderef used for generation of invoice total line items for this
3422 usage_class. FORMAT is either html or latex
3426 # should not be used: will have issues with hash element names (description vs
3427 # total_item and amount vs total_amount -- another array of functions?
3429 sub _condensed_total_line_generator {
3430 my ( $self, $format ) = ( shift, shift );
3432 my ( $f, $prefix, $suffix, $separator, $column ) =
3433 _condensed_generator_defaults($format);
3436 if ($format eq 'latex') {
3439 $separator = " & \n";
3441 sub { my ($d,$a,$s,$w) = @_;
3442 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3444 }elsif ( $format eq 'html' ) {
3448 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3450 sub { my ($d,$a,$s,$w) = @_;
3451 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3460 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3462 &{$column}( &{$f->{fields}->[$i]}(@args),
3463 map { $f->{$_}->[$i] } qw(align span width)
3467 $prefix. join( $separator, @result ). $suffix;
3472 #sub _items_extra_usage_sections {
3474 # my $escape = shift;
3476 # my %sections = ();
3478 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3479 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3481 # next unless $cust_bill_pkg->pkgnum > 0;
3483 # foreach my $section ( keys %usage_class ) {
3485 # my $usage = $cust_bill_pkg->usage($section);
3487 # next unless $usage && $usage > 0;
3489 # $sections{$section} ||= 0;
3490 # $sections{$section} += $usage;
3496 # map { { 'description' => &{$escape}($_),
3497 # 'subtotal' => $sections{$_},
3498 # 'summarized' => '',
3499 # 'tax_section' => '',
3502 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3506 sub _items_extra_usage_sections {
3515 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3516 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3517 next unless $cust_bill_pkg->pkgnum > 0;
3519 foreach my $classnum ( keys %usage_class ) {
3520 my $section = $usage_class{$classnum}->classname;
3521 $classnums{$section} = $classnum;
3523 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3524 my $amount = $detail->amount;
3525 next unless $amount && $amount > 0;
3527 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3528 $sections{$section}{amount} += $amount; #subtotal
3529 $sections{$section}{calls}++;
3530 $sections{$section}{duration} += $detail->duration;
3532 my $desc = $detail->regionname;
3533 my $description = $desc;
3534 $description = substr($desc, 0, 50). '...'
3535 if $format eq 'latex' && length($desc) > 50;
3537 $lines{$section}{$desc} ||= {
3538 description => &{$escape}($description),
3539 #pkgpart => $part_pkg->pkgpart,
3540 pkgnum => $cust_bill_pkg->pkgnum,
3545 #unit_amount => $cust_bill_pkg->unitrecur,
3546 quantity => $cust_bill_pkg->quantity,
3547 product_code => 'N/A',
3548 ext_description => [],
3551 $lines{$section}{$desc}{amount} += $amount;
3552 $lines{$section}{$desc}{calls}++;
3553 $lines{$section}{$desc}{duration} += $detail->duration;
3559 my %sectionmap = ();
3560 foreach (keys %sections) {
3561 my $usage_class = $usage_class{$classnums{$_}};
3562 $sectionmap{$_} = { 'description' => &{$escape}($_),
3563 'amount' => $sections{$_}{amount}, #subtotal
3564 'calls' => $sections{$_}{calls},
3565 'duration' => $sections{$_}{duration},
3567 'tax_section' => '',
3568 'sort_weight' => $usage_class->weight,
3569 ( $usage_class->format
3570 ? ( map { $_ => $usage_class->$_($format) }
3571 qw( description_generator header_generator total_generator total_line_generator )
3578 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3582 foreach my $section ( keys %lines ) {
3583 foreach my $line ( keys %{$lines{$section}} ) {
3584 my $l = $lines{$section}{$line};
3585 $l->{section} = $sectionmap{$section};
3586 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3587 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3592 return(\@sections, \@lines);
3596 sub _items_svc_phone_sections {
3605 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3607 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3608 next unless $cust_bill_pkg->pkgnum > 0;
3610 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3612 my $phonenum = $detail->phonenum;
3613 next unless $phonenum;
3615 my $amount = $detail->amount;
3616 next unless $amount && $amount > 0;
3618 $sections{$phonenum} ||= { 'amount' => 0,
3621 'sort_weight' => -1,
3622 'phonenum' => $phonenum,
3624 $sections{$phonenum}{amount} += $amount; #subtotal
3625 $sections{$phonenum}{calls}++;
3626 $sections{$phonenum}{duration} += $detail->duration;
3628 my $desc = $detail->regionname;
3629 my $description = $desc;
3630 $description = substr($desc, 0, 50). '...'
3631 if $format eq 'latex' && length($desc) > 50;
3633 $lines{$phonenum}{$desc} ||= {
3634 description => &{$escape}($description),
3635 #pkgpart => $part_pkg->pkgpart,
3643 product_code => 'N/A',
3644 ext_description => [],
3647 $lines{$phonenum}{$desc}{amount} += $amount;
3648 $lines{$phonenum}{$desc}{calls}++;
3649 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3651 my $line = $usage_class{$detail->classnum}->classname;
3652 $sections{"$phonenum $line"} ||=
3656 'sort_weight' => $usage_class{$detail->classnum}->weight,
3657 'phonenum' => $phonenum,
3659 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3660 $sections{"$phonenum $line"}{calls}++;
3661 $sections{"$phonenum $line"}{duration} += $detail->duration;
3663 $lines{"$phonenum $line"}{$desc} ||= {
3664 description => &{$escape}($description),
3665 #pkgpart => $part_pkg->pkgpart,
3673 product_code => 'N/A',
3674 ext_description => [],
3677 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3678 $lines{"$phonenum $line"}{$desc}{calls}++;
3679 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3680 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3681 $detail->formatted('format' => $format);
3686 my %sectionmap = ();
3687 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3688 my $minimal = new FS::usage_class { format => 'minimal' }; #bleh
3689 foreach ( keys %sections ) {
3690 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3691 my $usage_class = $summary ? $simple : $minimal;
3692 my $ending = $summary ? ' usage charges' : '';
3693 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3694 'amount' => $sections{$_}{amount}, #subtotal
3695 'calls' => $sections{$_}{calls},
3696 'duration' => $sections{$_}{duration},
3698 'tax_section' => '',
3699 'phonenum' => $sections{$_}{phonenum},
3700 'sort_weight' => $sections{$_}{sort_weight},
3702 ( map { $_ => $usage_class->$_($format) }
3703 qw( description_generator
3706 total_line_generator
3713 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3714 $a->{sort_weight} <=> $b->{sort_weight}
3719 foreach my $section ( keys %lines ) {
3720 foreach my $line ( keys %{$lines{$section}} ) {
3721 my $l = $lines{$section}{$line};
3722 $l->{section} = $sectionmap{$section};
3723 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3724 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3729 return(\@sections, \@lines);
3736 #my @display = scalar(@_)
3738 # : qw( _items_previous _items_pkg );
3739 # #: qw( _items_pkg );
3740 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3741 my @display = qw( _items_previous _items_pkg );
3744 foreach my $display ( @display ) {
3745 push @b, $self->$display(@_);
3750 sub _items_previous {
3752 my $cust_main = $self->cust_main;
3753 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3755 foreach ( @pr_cust_bill ) {
3757 'description' => 'Previous Balance, Invoice #'. $_->invnum.
3758 ' ('. time2str('%x',$_->_date). ')',
3759 #'pkgpart' => 'N/A',
3761 'amount' => sprintf("%.2f", $_->owed),
3767 # 'description' => 'Previous Balance',
3768 # #'pkgpart' => 'N/A',
3769 # 'pkgnum' => 'N/A',
3770 # 'amount' => sprintf("%10.2f", $pr_total ),
3771 # 'ext_description' => [ map {
3772 # "Invoice ". $_->invnum.
3773 # " (". time2str("%x",$_->_date). ") ".
3774 # sprintf("%10.2f", $_->owed)
3775 # } @pr_cust_bill ],
3783 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3784 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3785 if ($options{section} && $options{section}->{condensed}) {
3787 local $Storable::canonical = 1;
3788 foreach ( @items ) {
3790 delete $item->{ref};
3791 delete $item->{ext_description};
3792 my $key = freeze($item);
3793 $itemshash{$key} ||= 0;
3794 $itemshash{$key} ++; # += $item->{quantity};
3796 @items = sort { $a->{description} cmp $b->{description} }
3797 map { my $i = thaw($_);
3798 $i->{quantity} = $itemshash{$_};
3800 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3809 return 0 unless $a cmp $b;
3810 return -1 if $b eq 'Tax';
3811 return 1 if $a eq 'Tax';
3812 return -1 if $b eq 'Other surcharges';
3813 return 1 if $a eq 'Other surcharges';
3819 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3820 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3823 sub _items_cust_bill_pkg {
3825 my $cust_bill_pkg = shift;
3828 my $format = $opt{format} || '';
3829 my $escape_function = $opt{escape_function} || sub { shift };
3830 my $format_function = $opt{format_function} || '';
3831 my $unsquelched = $opt{unsquelched} || '';
3832 my $section = $opt{section}->{description} if $opt{section};
3833 my $summary_page = $opt{summary_page} || '';
3836 my ($s, $r, $u) = ( undef, undef, undef );
3837 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3840 foreach ( $s, $r, $u ) {
3841 if ( $_ && !$cust_bill_pkg->hidden ) {
3842 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3843 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3849 foreach my $display ( grep { defined($section)
3850 ? $_->section eq $section
3853 grep { !$_->summary || !$summary_page }
3854 $cust_bill_pkg->cust_bill_pkg_display
3858 my $type = $display->type;
3860 my $desc = $cust_bill_pkg->desc;
3861 $desc = substr($desc, 0, 50). '...'
3862 if $format eq 'latex' && length($desc) > 50;
3864 my %details_opt = ( 'format' => $format,
3865 'escape_function' => $escape_function,
3866 'format_function' => $format_function,
3869 if ( $cust_bill_pkg->pkgnum > 0 ) {
3871 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3873 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3875 my $description = $desc;
3876 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3879 push @d, map &{$escape_function}($_),
3880 $cust_pkg->h_labels_short($self->_date)
3881 unless $cust_pkg->part_pkg->hide_svc_detail
3882 || $cust_bill_pkg->hidden;
3883 push @d, $cust_bill_pkg->details(%details_opt)
3884 if $cust_bill_pkg->recur == 0;
3886 if ( $cust_bill_pkg->hidden ) {
3887 $s->{amount} += $cust_bill_pkg->setup;
3888 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3889 push @{ $s->{ext_description} }, @d;
3892 description => $description,
3893 #pkgpart => $part_pkg->pkgpart,
3894 pkgnum => $cust_bill_pkg->pkgnum,
3895 amount => $cust_bill_pkg->setup,
3896 unit_amount => $cust_bill_pkg->unitsetup,
3897 quantity => $cust_bill_pkg->quantity,
3898 ext_description => \@d,
3904 if ( $cust_bill_pkg->recur != 0 &&
3905 ( !$type || $type eq 'R' || $type eq 'U' )
3909 my $is_summary = $display->summary;
3910 my $description = ($is_summary && $type && $type eq 'U')
3911 ? "Usage charges" : $desc;
3913 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3914 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3915 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3920 #at least until cust_bill_pkg has "past" ranges in addition to
3921 #the "future" sdate/edate ones... see #3032
3922 my @dates = ( $self->_date );
3923 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3924 push @dates, $prev->sdate if $prev;
3926 push @d, map &{$escape_function}($_),
3927 $cust_pkg->h_labels_short(@dates)
3928 #$cust_bill_pkg->edate,
3929 #$cust_bill_pkg->sdate)
3930 unless $cust_pkg->part_pkg->hide_svc_detail
3931 || $cust_bill_pkg->itemdesc
3932 || $cust_bill_pkg->hidden
3933 || $is_summary && $type && $type eq 'U';
3935 push @d, $cust_bill_pkg->details(%details_opt)
3936 unless ($is_summary || $type && $type eq 'R');
3940 $amount = $cust_bill_pkg->recur;
3941 }elsif($type eq 'R') {
3942 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3943 }elsif($type eq 'U') {
3944 $amount = $cust_bill_pkg->usage;
3947 if ( !$type || $type eq 'R' ) {
3949 if ( $cust_bill_pkg->hidden ) {
3950 $r->{amount} += $amount;
3951 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3952 push @{ $r->{ext_description} }, @d;
3955 description => $description,
3956 #pkgpart => $part_pkg->pkgpart,
3957 pkgnum => $cust_bill_pkg->pkgnum,
3959 unit_amount => $cust_bill_pkg->unitrecur,
3960 quantity => $cust_bill_pkg->quantity,
3961 ext_description => \@d,
3965 } elsif ( $amount ) { # && $type eq 'U'
3967 if ( $cust_bill_pkg->hidden ) {
3968 $u->{amount} += $amount;
3969 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3970 push @{ $u->{ext_description} }, @d;
3973 description => $description,
3974 #pkgpart => $part_pkg->pkgpart,
3975 pkgnum => $cust_bill_pkg->pkgnum,
3977 unit_amount => $cust_bill_pkg->unitrecur,
3978 quantity => $cust_bill_pkg->quantity,
3979 ext_description => \@d,
3985 } # recurring or usage with recurring charge
3987 } else { #pkgnum tax or one-shot line item (??)
3989 if ( $cust_bill_pkg->setup != 0 ) {
3991 'description' => $desc,
3992 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3995 if ( $cust_bill_pkg->recur != 0 ) {
3997 'description' => "$desc (".
3998 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3999 time2str("%x", $cust_bill_pkg->edate). ')',
4000 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4010 foreach ( $s, $r, $u ) {
4012 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4013 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4022 sub _items_credits {
4023 my( $self, %opt ) = @_;
4024 my $trim_len = $opt{'trim_len'} || 60;
4028 foreach ( $self->cust_credited ) {
4030 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4032 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4033 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4034 $reason = " ($reason) " if $reason;
4037 #'description' => 'Credit ref\#'. $_->crednum.
4038 # " (". time2str("%x",$_->cust_credit->_date) .")".
4040 'description' => 'Credit applied '.
4041 time2str("%x",$_->cust_credit->_date). $reason,
4042 'amount' => sprintf("%.2f",$_->amount),
4050 sub _items_payments {
4054 #get & print payments
4055 foreach ( $self->cust_bill_pay ) {
4057 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4060 'description' => "Payment received ".
4061 time2str("%x",$_->cust_pay->_date ),
4062 'amount' => sprintf("%.2f", $_->amount )
4070 =item call_details [ OPTION => VALUE ... ]
4072 Returns an array of CSV strings representing the call details for this invoice
4073 The only option available is the boolean prepend_billed_number
4078 my ($self, %opt) = @_;
4080 my $format_function = sub { shift };
4082 if ($opt{prepend_billed_number}) {
4083 $format_function = sub {
4087 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4092 my @details = map { $_->details( 'format_function' => $format_function,
4093 'escape_function' => sub{ return() },
4097 $self->cust_bill_pkg;
4098 my $header = $details[0];
4099 ( $header, grep { $_ ne $header } @details );
4109 =item process_reprint
4113 sub process_reprint {
4114 process_re_X('print', @_);
4117 =item process_reemail
4121 sub process_reemail {
4122 process_re_X('email', @_);
4130 process_re_X('fax', @_);
4138 process_re_X('ftp', @_);
4145 sub process_respool {
4146 process_re_X('spool', @_);
4149 use Storable qw(thaw);
4153 my( $method, $job ) = ( shift, shift );
4154 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4156 my $param = thaw(decode_base64(shift));
4157 warn Dumper($param) if $DEBUG;
4168 my($method, $job, %param ) = @_;
4170 warn "re_X $method for job $job with param:\n".
4171 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4174 #some false laziness w/search/cust_bill.html
4176 my $orderby = 'ORDER BY cust_bill._date';
4178 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
4180 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4182 my @cust_bill = qsearch( {
4183 #'select' => "cust_bill.*",
4184 'table' => 'cust_bill',
4185 'addl_from' => $addl_from,
4187 'extra_sql' => $extra_sql,
4188 'order_by' => $orderby,
4192 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4194 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4197 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4198 foreach my $cust_bill ( @cust_bill ) {
4199 $cust_bill->$method();
4201 if ( $job ) { #progressbar foo
4203 if ( time - $min_sec > $last ) {
4204 my $error = $job->update_statustext(
4205 int( 100 * $num / scalar(@cust_bill) )
4207 die $error if $error;
4218 =head1 CLASS METHODS
4224 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4230 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4235 Returns an SQL fragment to retreive the net amount (charged minus credited).
4241 'charged - '. $class->credited_sql;
4246 Returns an SQL fragment to retreive the amount paid against this invoice.
4252 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4253 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4258 Returns an SQL fragment to retreive the amount credited against this invoice.
4264 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4265 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4268 =item search_sql HASHREF
4270 Class method which returns an SQL WHERE fragment to search for parameters
4271 specified in HASHREF. Valid parameters are
4277 List reference of start date, end date, as UNIX timestamps.
4287 List reference of charged limits (exclusive).
4291 List reference of charged limits (exclusive).
4295 flag, return open invoices only
4299 flag, return net invoices only
4303 =item newest_percust
4307 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4312 my($class, $param) = @_;
4314 warn "$me search_sql called with params: \n".
4315 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4321 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4322 push @search, "cust_main.agentnum = $1";
4326 if ( $param->{_date} ) {
4327 my($beginning, $ending) = @{$param->{_date}};
4329 push @search, "cust_bill._date >= $beginning",
4330 "cust_bill._date < $ending";
4334 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4335 push @search, "cust_bill.invnum >= $1";
4337 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4338 push @search, "cust_bill.invnum <= $1";
4342 if ( $param->{charged} ) {
4343 my @charged = ref($param->{charged})
4344 ? @{ $param->{charged} }
4345 : ($param->{charged});
4347 push @search, map { s/^charged/cust_bill.charged/; $_; }
4351 my $owed_sql = FS::cust_bill->owed_sql;
4354 if ( $param->{owed} ) {
4355 my @owed = ref($param->{owed})
4356 ? @{ $param->{owed} }
4358 push @search, map { s/^owed/$owed_sql/; $_; }
4363 push @search, "0 != $owed_sql"
4364 if $param->{'open'};
4365 push @search, '0 != '. FS::cust_bill->net_sql
4369 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4370 if $param->{'days'};
4373 if ( $param->{'newest_percust'} ) {
4375 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4376 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4378 my @newest_where = map { my $x = $_;
4379 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4382 grep ! /^cust_main./, @search;
4383 my $newest_where = scalar(@newest_where)
4384 ? ' AND '. join(' AND ', @newest_where)
4388 push @search, "cust_bill._date = (
4389 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4390 WHERE newest_cust_bill.custnum = cust_bill.custnum
4396 #agent virtualization
4397 my $curuser = $FS::CurrentUser::CurrentUser;
4398 if ( $curuser->username eq 'fs_queue'
4399 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4401 my $newuser = qsearchs('access_user', {
4402 'username' => $username,
4406 $curuser = $newuser;
4408 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4411 push @search, $curuser->agentnums_sql;
4413 join(' AND ', @search );
4425 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4426 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base