4 use vars qw( @ISA $DEBUG $me $conf $money_char $date_format );
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') || '$';
47 $date_format = $conf->config('date_format') || '%x';
52 FS::cust_bill - Object methods for cust_bill records
58 $record = new FS::cust_bill \%hash;
59 $record = new FS::cust_bill { 'column' => 'value' };
61 $error = $record->insert;
63 $error = $new_record->replace($old_record);
65 $error = $record->delete;
67 $error = $record->check;
69 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
71 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
73 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
75 @cust_pay_objects = $cust_bill->cust_pay;
77 $tax_amount = $record->tax;
79 @lines = $cust_bill->print_text;
80 @lines = $cust_bill->print_text $time;
84 An FS::cust_bill object represents an invoice; a declaration that a customer
85 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
86 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
87 following fields are currently supported:
93 =item invnum - primary key (assigned automatically for new invoices)
95 =item custnum - customer (see L<FS::cust_main>)
97 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
98 L<Time::Local> and L<Date::Parse> for conversion functions.
100 =item charged - amount of this invoice
102 =item invoice_terms - optional terms override for this specific invoice
106 Customer info at invoice generation time
110 =item previous_balance
112 =item billing_balance
120 =item printed - deprecated
128 =item closed - books closed flag, empty or `Y'
130 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
132 =item agent_invid - legacy invoice number
142 Creates a new invoice. To add the invoice to the database, see L<"insert">.
143 Invoices are normally created by calling the bill method of a customer object
144 (see L<FS::cust_main>).
148 sub table { 'cust_bill'; }
150 sub cust_linked { $_[0]->cust_main_custnum; }
151 sub cust_unlinked_msg {
153 "WARNING: can't find cust_main.custnum ". $self->custnum.
154 ' (cust_bill.invnum '. $self->invnum. ')';
159 Adds this invoice to the database ("Posts" the invoice). If there is an error,
160 returns the error, otherwise returns false.
164 This method now works but you probably shouldn't use it. Instead, apply a
165 credit against the invoice.
167 Using this method to delete invoices outright is really, really bad. There
168 would be no record you ever posted this invoice, and there are no check to
169 make sure charged = 0 or that there are no associated cust_bill_pkg records.
171 Really, don't use it.
177 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
179 local $SIG{HUP} = 'IGNORE';
180 local $SIG{INT} = 'IGNORE';
181 local $SIG{QUIT} = 'IGNORE';
182 local $SIG{TERM} = 'IGNORE';
183 local $SIG{TSTP} = 'IGNORE';
184 local $SIG{PIPE} = 'IGNORE';
186 my $oldAutoCommit = $FS::UID::AutoCommit;
187 local $FS::UID::AutoCommit = 0;
190 foreach my $table (qw(
202 foreach my $linked ( $self->$table() ) {
203 my $error = $linked->delete;
205 $dbh->rollback if $oldAutoCommit;
212 my $error = $self->SUPER::delete(@_);
214 $dbh->rollback if $oldAutoCommit;
218 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
224 =item replace OLD_RECORD
226 Replaces the OLD_RECORD with this one in the database. If there is an error,
227 returns the error, otherwise returns false.
229 Only printed may be changed. printed is normally updated by calling the
230 collect method of a customer object (see L<FS::cust_main>).
234 #replace can be inherited from Record.pm
236 # replace_check is now the preferred way to #implement replace data checks
237 # (so $object->replace() works without an argument)
240 my( $new, $old ) = ( shift, shift );
241 return "Can't change custnum!" unless $old->custnum == $new->custnum;
242 #return "Can't change _date!" unless $old->_date eq $new->_date;
243 return "Can't change _date!" unless $old->_date == $new->_date;
244 return "Can't change charged!" unless $old->charged == $new->charged
245 || $old->charged == 0;
252 Checks all fields to make sure this is a valid invoice. If there is an error,
253 returns the error, otherwise returns false. Called by the insert and replace
262 $self->ut_numbern('invnum')
263 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
264 || $self->ut_numbern('_date')
265 || $self->ut_money('charged')
266 || $self->ut_numbern('printed')
267 || $self->ut_enum('closed', [ '', 'Y' ])
268 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
269 || $self->ut_numbern('agent_invid') #varchar?
271 return $error if $error;
273 $self->_date(time) unless $self->_date;
275 $self->printed(0) if $self->printed eq '';
282 Returns the displayed invoice number for this invoice: agent_invid if
283 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
289 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
290 return $self->agent_invid;
292 return $self->invnum;
298 Returns a list consisting of the total previous balance for this customer,
299 followed by the previous outstanding invoices (as FS::cust_bill objects also).
306 my @cust_bill = sort { $a->_date <=> $b->_date }
307 grep { $_->owed != 0 && $_->_date < $self->_date }
308 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
310 foreach ( @cust_bill ) { $total += $_->owed; }
316 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
323 { 'table' => 'cust_bill_pkg',
324 'hashref' => { 'invnum' => $self->invnum },
325 'order_by' => 'ORDER BY billpkgnum',
330 =item cust_bill_pkg_pkgnum PKGNUM
332 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
337 sub cust_bill_pkg_pkgnum {
338 my( $self, $pkgnum ) = @_;
340 { 'table' => 'cust_bill_pkg',
341 'hashref' => { 'invnum' => $self->invnum,
344 'order_by' => 'ORDER BY billpkgnum',
351 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
358 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
360 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
363 =item open_cust_bill_pkg
365 Returns the open line items for this invoice.
367 Note that cust_bill_pkg with both setup and recur fees are returned as two
368 separate line items, each with only one fee.
372 # modeled after cust_main::open_cust_bill
373 sub open_cust_bill_pkg {
376 # grep { $_->owed > 0 } $self->cust_bill_pkg
378 my %other = ( 'recur' => 'setup',
379 'setup' => 'recur', );
381 foreach my $field ( qw( recur setup )) {
382 push @open, map { $_->set( $other{$field}, 0 ); $_; }
383 grep { $_->owed($field) > 0 }
384 $self->cust_bill_pkg;
390 =item cust_bill_event
392 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
396 sub cust_bill_event {
398 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
401 =item num_cust_bill_event
403 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
407 sub num_cust_bill_event {
410 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
411 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
412 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
413 $sth->fetchrow_arrayref->[0];
418 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
422 #false laziness w/cust_pkg.pm
426 'table' => 'cust_event',
427 'addl_from' => 'JOIN part_event USING ( eventpart )',
428 'hashref' => { 'tablenum' => $self->invnum },
429 'extra_sql' => " AND eventtable = 'cust_bill' ",
435 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
439 #false laziness w/cust_pkg.pm
443 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
444 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
445 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
446 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
447 $sth->fetchrow_arrayref->[0];
452 Returns the customer (see L<FS::cust_main>) for this invoice.
458 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
461 =item cust_suspend_if_balance_over AMOUNT
463 Suspends the customer associated with this invoice if the total amount owed on
464 this invoice and all older invoices is greater than the specified amount.
466 Returns a list: an empty list on success or a list of errors.
470 sub cust_suspend_if_balance_over {
471 my( $self, $amount ) = ( shift, shift );
472 my $cust_main = $self->cust_main;
473 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
476 $cust_main->suspend(@_);
482 Depreciated. See the cust_credited method.
484 #Returns a list consisting of the total previous credited (see
485 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
486 #outstanding credits (FS::cust_credit objects).
492 croak "FS::cust_bill->cust_credit depreciated; see ".
493 "FS::cust_bill->cust_credit_bill";
496 #my @cust_credit = sort { $a->_date <=> $b->_date }
497 # grep { $_->credited != 0 && $_->_date < $self->_date }
498 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
500 #foreach (@cust_credit) { $total += $_->credited; }
501 #$total, @cust_credit;
506 Depreciated. See the cust_bill_pay method.
508 #Returns all payments (see L<FS::cust_pay>) for this invoice.
514 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
516 #sort { $a->_date <=> $b->_date }
517 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
523 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
526 sub cust_bill_pay_batch {
528 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
533 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
539 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
540 sort { $a->_date <=> $b->_date }
541 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
546 =item cust_credit_bill
548 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
554 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
555 sort { $a->_date <=> $b->_date }
556 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
560 sub cust_credit_bill {
561 shift->cust_credited(@_);
564 =item cust_bill_pay_pkgnum PKGNUM
566 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
567 with matching pkgnum.
571 sub cust_bill_pay_pkgnum {
572 my( $self, $pkgnum ) = @_;
573 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
574 sort { $a->_date <=> $b->_date }
575 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
581 =item cust_credited_pkgnum PKGNUM
583 =item cust_credit_bill_pkgnum PKGNUM
585 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
586 with matching pkgnum.
590 sub cust_credited_pkgnum {
591 my( $self, $pkgnum ) = @_;
592 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
593 sort { $a->_date <=> $b->_date }
594 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
600 sub cust_credit_bill_pkgnum {
601 shift->cust_credited_pkgnum(@_);
606 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
613 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
615 foreach (@taxlines) { $total += $_->setup; }
621 Returns the amount owed (still outstanding) on this invoice, which is charged
622 minus all payment applications (see L<FS::cust_bill_pay>) and credit
623 applications (see L<FS::cust_credit_bill>).
629 my $balance = $self->charged;
630 $balance -= $_->amount foreach ( $self->cust_bill_pay );
631 $balance -= $_->amount foreach ( $self->cust_credited );
632 $balance = sprintf( "%.2f", $balance);
633 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
638 my( $self, $pkgnum ) = @_;
640 #my $balance = $self->charged;
642 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
644 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
645 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
647 $balance = sprintf( "%.2f", $balance);
648 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
652 =item apply_payments_and_credits [ OPTION => VALUE ... ]
654 Applies unapplied payments and credits to this invoice.
656 A hash of optional arguments may be passed. Currently "manual" is supported.
657 If true, a payment receipt is sent instead of a statement when
658 'payment_receipt_email' configuration option is set.
660 If there is an error, returns the error, otherwise returns false.
664 sub apply_payments_and_credits {
665 my( $self, %options ) = @_;
667 local $SIG{HUP} = 'IGNORE';
668 local $SIG{INT} = 'IGNORE';
669 local $SIG{QUIT} = 'IGNORE';
670 local $SIG{TERM} = 'IGNORE';
671 local $SIG{TSTP} = 'IGNORE';
672 local $SIG{PIPE} = 'IGNORE';
674 my $oldAutoCommit = $FS::UID::AutoCommit;
675 local $FS::UID::AutoCommit = 0;
678 $self->select_for_update; #mutex
680 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
681 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
683 if ( $conf->exists('pkg-balances') ) {
684 # limit @payments & @credits to those w/ a pkgnum grepped from $self
685 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
686 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
687 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
690 while ( $self->owed > 0 and ( @payments || @credits ) ) {
693 if ( @payments && @credits ) {
695 #decide which goes first by weight of top (unapplied) line item
697 my @open_lineitems = $self->open_cust_bill_pkg;
700 max( map { $_->part_pkg->pay_weight || 0 }
705 my $max_credit_weight =
706 max( map { $_->part_pkg->credit_weight || 0 }
712 #if both are the same... payments first? it has to be something
713 if ( $max_pay_weight >= $max_credit_weight ) {
719 } elsif ( @payments ) {
721 } elsif ( @credits ) {
724 die "guru meditation #12 and 35";
728 if ( $app eq 'pay' ) {
730 my $payment = shift @payments;
731 $unapp_amount = $payment->unapplied;
732 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
733 $app->pkgnum( $payment->pkgnum )
734 if $conf->exists('pkg-balances') && $payment->pkgnum;
736 } elsif ( $app eq 'credit' ) {
738 my $credit = shift @credits;
739 $unapp_amount = $credit->credited;
740 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
741 $app->pkgnum( $credit->pkgnum )
742 if $conf->exists('pkg-balances') && $credit->pkgnum;
745 die "guru meditation #12 and 35";
749 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
750 warn "owed_pkgnum ". $app->pkgnum;
751 $owed = $self->owed_pkgnum($app->pkgnum);
755 next unless $owed > 0;
757 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
758 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
760 $app->invnum( $self->invnum );
762 my $error = $app->insert(%options);
764 $dbh->rollback if $oldAutoCommit;
765 return "Error inserting ". $app->table. " record: $error";
767 die $error if $error;
771 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
776 =item generate_email OPTION => VALUE ...
784 sender address, required
788 alternate template name, optional
792 text attachment arrayref, optional
796 email subject, optional
800 notice name instead of "Invoice", optional
804 Returns an argument list to be passed to L<FS::Misc::send_email>.
815 my $me = '[FS::cust_bill::generate_email]';
818 'from' => $args{'from'},
819 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
823 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
824 'template' => $args{'template'},
825 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
828 my $cust_main = $self->cust_main;
830 if (ref($args{'to'}) eq 'ARRAY') {
831 $return{'to'} = $args{'to'};
833 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
834 $cust_main->invoicing_list
838 if ( $conf->exists('invoice_html') ) {
840 warn "$me creating HTML/text multipart message"
843 $return{'nobody'} = 1;
845 my $alternative = build MIME::Entity
846 'Type' => 'multipart/alternative',
847 'Encoding' => '7bit',
848 'Disposition' => 'inline'
852 if ( $conf->exists('invoice_email_pdf')
853 and scalar($conf->config('invoice_email_pdf_note')) ) {
855 warn "$me using 'invoice_email_pdf_note' in multipart message"
857 $data = [ map { $_ . "\n" }
858 $conf->config('invoice_email_pdf_note')
863 warn "$me not using 'invoice_email_pdf_note' in multipart message"
865 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
866 $data = $args{'print_text'};
868 $data = [ $self->print_text(\%opt) ];
873 $alternative->attach(
874 'Type' => 'text/plain',
875 #'Encoding' => 'quoted-printable',
876 'Encoding' => '7bit',
878 'Disposition' => 'inline',
881 $args{'from'} =~ /\@([\w\.\-]+)/;
882 my $from = $1 || 'example.com';
883 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
886 my $agentnum = $cust_main->agentnum;
887 if ( defined($args{'template'}) && length($args{'template'})
888 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
891 $logo = 'logo_'. $args{'template'}. '.png';
895 my $image_data = $conf->config_binary( $logo, $agentnum);
897 my $image = build MIME::Entity
898 'Type' => 'image/png',
899 'Encoding' => 'base64',
900 'Data' => $image_data,
901 'Filename' => 'logo.png',
902 'Content-ID' => "<$content_id>",
905 $alternative->attach(
906 'Type' => 'text/html',
907 'Encoding' => 'quoted-printable',
908 'Data' => [ '<html>',
911 ' '. encode_entities($return{'subject'}),
914 ' <body bgcolor="#e8e8e8">',
915 $self->print_html({ 'cid'=>$content_id, %opt }),
919 'Disposition' => 'inline',
920 #'Filename' => 'invoice.pdf',
924 if ( $cust_main->email_csv_cdr ) {
926 push @otherparts, build MIME::Entity
927 'Type' => 'text/csv',
928 'Encoding' => '7bit',
929 'Data' => [ map { "$_\n" }
930 $self->call_details('prepend_billed_number' => 1)
932 'Disposition' => 'attachment',
933 'Filename' => 'usage-'. $self->invnum. '.csv',
938 if ( $conf->exists('invoice_email_pdf') ) {
943 # multipart/alternative
949 my $related = build MIME::Entity 'Type' => 'multipart/related',
950 'Encoding' => '7bit';
952 #false laziness w/Misc::send_email
953 $related->head->replace('Content-type',
955 '; boundary="'. $related->head->multipart_boundary. '"'.
956 '; type=multipart/alternative'
959 $related->add_part($alternative);
961 $related->add_part($image);
963 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
965 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
969 #no other attachment:
971 # multipart/alternative
976 $return{'content-type'} = 'multipart/related';
977 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
978 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
979 #$return{'disposition'} = 'inline';
985 if ( $conf->exists('invoice_email_pdf') ) {
986 warn "$me creating PDF attachment"
989 #mime parts arguments a la MIME::Entity->build().
990 $return{'mimeparts'} = [
991 { $self->mimebuild_pdf(\%opt) }
995 if ( $conf->exists('invoice_email_pdf')
996 and scalar($conf->config('invoice_email_pdf_note')) ) {
998 warn "$me using 'invoice_email_pdf_note'"
1000 $return{'body'} = [ map { $_ . "\n" }
1001 $conf->config('invoice_email_pdf_note')
1006 warn "$me not using 'invoice_email_pdf_note'"
1008 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1009 $return{'body'} = $args{'print_text'};
1011 $return{'body'} = [ $self->print_text(\%opt) ];
1024 Returns a list suitable for passing to MIME::Entity->build(), representing
1025 this invoice as PDF attachment.
1032 'Type' => 'application/pdf',
1033 'Encoding' => 'base64',
1034 'Data' => [ $self->print_pdf(@_) ],
1035 'Disposition' => 'attachment',
1036 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1040 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1042 Sends this invoice to the destinations configured for this customer: sends
1043 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1045 Options can be passed as a hashref (recommended) or as a list of up to
1046 four values for templatename, agentnum, invoice_from and amount.
1048 I<template>, if specified, is the name of a suffix for alternate invoices.
1050 I<agentnum>, if specified, means that this invoice will only be sent for customers
1051 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1052 single agent) or an arrayref of agentnums.
1054 I<invoice_from>, if specified, overrides the default email invoice From: address.
1056 I<amount>, if specified, only sends the invoice if the total amount owed on this
1057 invoice and all older invoices is greater than the specified amount.
1059 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1063 sub queueable_send {
1066 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1067 or die "invalid invoice number: " . $opt{invnum};
1069 my @args = ( $opt{template}, $opt{agentnum} );
1070 push @args, $opt{invoice_from}
1071 if exists($opt{invoice_from}) && $opt{invoice_from};
1073 my $error = $self->send( @args );
1074 die $error if $error;
1081 my( $template, $invoice_from, $notice_name );
1083 my $balance_over = 0;
1087 $template = $opt->{'template'} || '';
1088 if ( $agentnums = $opt->{'agentnum'} ) {
1089 $agentnums = [ $agentnums ] unless ref($agentnums);
1091 $invoice_from = $opt->{'invoice_from'};
1092 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1093 $notice_name = $opt->{'notice_name'};
1095 $template = scalar(@_) ? shift : '';
1096 if ( scalar(@_) && $_[0] ) {
1097 $agentnums = ref($_[0]) ? shift : [ shift ];
1099 $invoice_from = shift if scalar(@_);
1100 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1103 return 'N/A' unless ! $agentnums
1104 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1107 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1109 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1110 $conf->config('invoice_from', $self->cust_main->agentnum );
1113 'template' => $template,
1114 'invoice_from' => $invoice_from,
1115 'notice_name' => ( $notice_name || 'Invoice' ),
1118 my @invoicing_list = $self->cust_main->invoicing_list;
1120 #$self->email_invoice(\%opt)
1122 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1124 #$self->print_invoice(\%opt)
1126 if grep { $_ eq 'POST' } @invoicing_list; #postal
1128 $self->fax_invoice(\%opt)
1129 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1135 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1137 Emails this invoice.
1139 Options can be passed as a hashref (recommended) or as a list of up to
1140 two values for templatename and invoice_from.
1142 I<template>, if specified, is the name of a suffix for alternate invoices.
1144 I<invoice_from>, if specified, overrides the default email invoice From: address.
1146 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1150 sub queueable_email {
1153 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1154 or die "invalid invoice number: " . $opt{invnum};
1156 my @args = ( $opt{template} );
1157 push @args, $opt{invoice_from}
1158 if exists($opt{invoice_from}) && $opt{invoice_from};
1160 my $error = $self->email( @args );
1161 die $error if $error;
1165 #sub email_invoice {
1169 my( $template, $invoice_from, $notice_name );
1172 $template = $opt->{'template'} || '';
1173 $invoice_from = $opt->{'invoice_from'};
1174 $notice_name = $opt->{'notice_name'} || 'Invoice';
1176 $template = scalar(@_) ? shift : '';
1177 $invoice_from = shift if scalar(@_);
1178 $notice_name = 'Invoice';
1181 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1182 $conf->config('invoice_from', $self->cust_main->agentnum );
1184 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1185 $self->cust_main->invoicing_list;
1187 #better to notify this person than silence
1188 @invoicing_list = ($invoice_from) unless @invoicing_list;
1190 my $subject = $self->email_subject($template);
1192 my $error = send_email(
1193 $self->generate_email(
1194 'from' => $invoice_from,
1195 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1196 'subject' => $subject,
1197 'template' => $template,
1198 'notice_name' => $notice_name,
1201 die "can't email invoice: $error\n" if $error;
1202 #die "$error\n" if $error;
1209 #my $template = scalar(@_) ? shift : '';
1212 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1215 my $cust_main = $self->cust_main;
1216 my $name = $cust_main->name;
1217 my $name_short = $cust_main->name_short;
1218 my $invoice_number = $self->invnum;
1219 my $invoice_date = $self->_date_pretty;
1221 eval qq("$subject");
1224 =item lpr_data HASHREF | [ TEMPLATE ]
1226 Returns the postscript or plaintext for this invoice as an arrayref.
1228 Options can be passed as a hashref (recommended) or as a single optional value
1231 I<template>, if specified, is the name of a suffix for alternate invoices.
1233 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1239 my( $template, $notice_name );
1242 $template = $opt->{'template'} || '';
1243 $notice_name = $opt->{'notice_name'} || 'Invoice';
1245 $template = scalar(@_) ? shift : '';
1246 $notice_name = 'Invoice';
1250 'template' => $template,
1251 'notice_name' => $notice_name,
1254 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1255 [ $self->$method( \%opt ) ];
1258 =item print HASHREF | [ TEMPLATE ]
1260 Prints this invoice.
1262 Options can be passed as a hashref (recommended) or as a single optional
1265 I<template>, if specified, is the name of a suffix for alternate invoices.
1267 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1271 #sub print_invoice {
1274 my( $template, $notice_name );
1277 $template = $opt->{'template'} || '';
1278 $notice_name = $opt->{'notice_name'} || 'Invoice';
1280 $template = scalar(@_) ? shift : '';
1281 $notice_name = 'Invoice';
1285 'template' => $template,
1286 'notice_name' => $notice_name,
1289 do_print $self->lpr_data(\%opt);
1292 =item fax_invoice HASHREF | [ TEMPLATE ]
1296 Options can be passed as a hashref (recommended) or as a single optional
1299 I<template>, if specified, is the name of a suffix for alternate invoices.
1301 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1307 my( $template, $notice_name );
1310 $template = $opt->{'template'} || '';
1311 $notice_name = $opt->{'notice_name'} || 'Invoice';
1313 $template = scalar(@_) ? shift : '';
1314 $notice_name = 'Invoice';
1317 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1318 unless $conf->exists('invoice_latex');
1320 my $dialstring = $self->cust_main->getfield('fax');
1324 'template' => $template,
1325 'notice_name' => $notice_name,
1328 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1329 'dialstring' => $dialstring,
1331 die $error if $error;
1335 =item ftp_invoice [ TEMPLATENAME ]
1337 Sends this invoice data via FTP.
1339 TEMPLATENAME is unused?
1345 my $template = scalar(@_) ? shift : '';
1348 'protocol' => 'ftp',
1349 'server' => $conf->config('cust_bill-ftpserver'),
1350 'username' => $conf->config('cust_bill-ftpusername'),
1351 'password' => $conf->config('cust_bill-ftppassword'),
1352 'dir' => $conf->config('cust_bill-ftpdir'),
1353 'format' => $conf->config('cust_bill-ftpformat'),
1357 =item spool_invoice [ TEMPLATENAME ]
1359 Spools this invoice data (see L<FS::spool_csv>)
1361 TEMPLATENAME is unused?
1367 my $template = scalar(@_) ? shift : '';
1370 'format' => $conf->config('cust_bill-spoolformat'),
1371 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1375 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1377 Like B<send>, but only sends the invoice if it is the newest open invoice for
1382 sub send_if_newest {
1387 grep { $_->owed > 0 }
1388 qsearch('cust_bill', {
1389 'custnum' => $self->custnum,
1390 #'_date' => { op=>'>', value=>$self->_date },
1391 'invnum' => { op=>'>', value=>$self->invnum },
1398 =item send_csv OPTION => VALUE, ...
1400 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1404 protocol - currently only "ftp"
1410 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1411 and YYMMDDHHMMSS is a timestamp.
1413 See L</print_csv> for a description of the output format.
1418 my($self, %opt) = @_;
1422 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1423 mkdir $spooldir, 0700 unless -d $spooldir;
1425 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1426 my $file = "$spooldir/$tracctnum.csv";
1428 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1430 open(CSV, ">$file") or die "can't open $file: $!";
1438 if ( $opt{protocol} eq 'ftp' ) {
1439 eval "use Net::FTP;";
1441 $net = Net::FTP->new($opt{server}) or die @$;
1443 die "unknown protocol: $opt{protocol}";
1446 $net->login( $opt{username}, $opt{password} )
1447 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1449 $net->binary or die "can't set binary mode";
1451 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1453 $net->put($file) or die "can't put $file: $!";
1463 Spools CSV invoice data.
1469 =item format - 'default' or 'billco'
1471 =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>).
1473 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1475 =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.
1482 my($self, %opt) = @_;
1484 my $cust_main = $self->cust_main;
1486 if ( $opt{'dest'} ) {
1487 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1488 $cust_main->invoicing_list;
1489 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1490 || ! keys %invoicing_list;
1493 if ( $opt{'balanceover'} ) {
1495 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1498 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1499 mkdir $spooldir, 0700 unless -d $spooldir;
1501 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1505 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1506 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1509 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1511 open(CSV, ">>$file") or die "can't open $file: $!";
1512 flock(CSV, LOCK_EX);
1517 if ( lc($opt{'format'}) eq 'billco' ) {
1519 flock(CSV, LOCK_UN);
1524 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1527 open(CSV,">>$file") or die "can't open $file: $!";
1528 flock(CSV, LOCK_EX);
1534 flock(CSV, LOCK_UN);
1541 =item print_csv OPTION => VALUE, ...
1543 Returns CSV data for this invoice.
1547 format - 'default' or 'billco'
1549 Returns a list consisting of two scalars. The first is a single line of CSV
1550 header information for this invoice. The second is one or more lines of CSV
1551 detail information for this invoice.
1553 If I<format> is not specified or "default", the fields of the CSV file are as
1556 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1560 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1562 B<record_type> is C<cust_bill> for the initial header line only. The
1563 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1564 fields are filled in.
1566 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1567 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1570 =item invnum - invoice number
1572 =item custnum - customer number
1574 =item _date - invoice date
1576 =item charged - total invoice amount
1578 =item first - customer first name
1580 =item last - customer first name
1582 =item company - company name
1584 =item address1 - address line 1
1586 =item address2 - address line 1
1596 =item pkg - line item description
1598 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1600 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1602 =item sdate - start date for recurring fee
1604 =item edate - end date for recurring fee
1608 If I<format> is "billco", the fields of the header CSV file are as follows:
1610 +-------------------------------------------------------------------+
1611 | FORMAT HEADER FILE |
1612 |-------------------------------------------------------------------|
1613 | Field | Description | Name | Type | Width |
1614 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1615 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1616 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1617 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1618 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1619 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1620 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1621 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1622 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1623 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1624 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1625 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1626 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1627 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1628 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1629 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1630 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1631 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1632 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1633 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1634 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1635 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1636 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1637 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1638 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1639 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1640 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1641 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1642 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1643 +-------+-------------------------------+------------+------+-------+
1645 If I<format> is "billco", the fields of the detail CSV file are as follows:
1647 FORMAT FOR DETAIL FILE
1649 Field | Description | Name | Type | Width
1650 1 | N/A-Leave Empty | RC | CHAR | 2
1651 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1652 3 | Account Number | TRACCTNUM | CHAR | 15
1653 4 | Invoice Number | TRINVOICE | CHAR | 15
1654 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1655 6 | Transaction Detail | DETAILS | CHAR | 100
1656 7 | Amount | AMT | NUM* | 9
1657 8 | Line Format Control** | LNCTRL | CHAR | 2
1658 9 | Grouping Code | GROUP | CHAR | 2
1659 10 | User Defined | ACCT CODE | CHAR | 15
1664 my($self, %opt) = @_;
1666 eval "use Text::CSV_XS";
1669 my $cust_main = $self->cust_main;
1671 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1673 if ( lc($opt{'format'}) eq 'billco' ) {
1676 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1678 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1680 my( $previous_balance, @unused ) = $self->previous; #previous balance
1682 my $pmt_cr_applied = 0;
1683 $pmt_cr_applied += $_->{'amount'}
1684 foreach ( $self->_items_payments, $self->_items_credits ) ;
1686 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1689 '', # 1 | N/A-Leave Empty CHAR 2
1690 '', # 2 | N/A-Leave Empty CHAR 15
1691 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1692 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1693 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1694 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1695 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1696 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1697 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1698 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1699 '', # 10 | Ancillary Billing Information CHAR 30
1700 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1701 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1704 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1707 $duedate, # 14 | Bill Due Date CHAR 10
1709 $previous_balance, # 15 | Previous Balance NUM* 9
1710 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1711 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1712 $totaldue, # 18 | Total Amt Due NUM* 9
1713 $totaldue, # 19 | Total Amt Due NUM* 9
1714 '', # 20 | 30 Day Aging NUM* 9
1715 '', # 21 | 60 Day Aging NUM* 9
1716 '', # 22 | 90 Day Aging NUM* 9
1717 'N', # 23 | Y/N CHAR 1
1718 '', # 24 | Remittance automation CHAR 100
1719 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1720 $self->custnum, # 26 | Customer Reference Number CHAR 15
1721 '0', # 27 | Federal Tax*** NUM* 9
1722 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1723 '0', # 29 | Other Taxes & Fees*** NUM* 9
1732 time2str("%x", $self->_date),
1733 sprintf("%.2f", $self->charged),
1734 ( map { $cust_main->getfield($_) }
1735 qw( first last company address1 address2 city state zip country ) ),
1737 ) or die "can't create csv";
1740 my $header = $csv->string. "\n";
1743 if ( lc($opt{'format'}) eq 'billco' ) {
1746 foreach my $item ( $self->_items_pkg ) {
1749 '', # 1 | N/A-Leave Empty CHAR 2
1750 '', # 2 | N/A-Leave Empty CHAR 15
1751 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1752 $self->invnum, # 4 | Invoice Number CHAR 15
1753 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1754 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1755 $item->{'amount'}, # 7 | Amount NUM* 9
1756 '', # 8 | Line Format Control** CHAR 2
1757 '', # 9 | Grouping Code CHAR 2
1758 '', # 10 | User Defined CHAR 15
1761 $detail .= $csv->string. "\n";
1767 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1769 my($pkg, $setup, $recur, $sdate, $edate);
1770 if ( $cust_bill_pkg->pkgnum ) {
1772 ($pkg, $setup, $recur, $sdate, $edate) = (
1773 $cust_bill_pkg->part_pkg->pkg,
1774 ( $cust_bill_pkg->setup != 0
1775 ? sprintf("%.2f", $cust_bill_pkg->setup )
1777 ( $cust_bill_pkg->recur != 0
1778 ? sprintf("%.2f", $cust_bill_pkg->recur )
1780 ( $cust_bill_pkg->sdate
1781 ? time2str("%x", $cust_bill_pkg->sdate)
1783 ($cust_bill_pkg->edate
1784 ?time2str("%x", $cust_bill_pkg->edate)
1788 } else { #pkgnum tax
1789 next unless $cust_bill_pkg->setup != 0;
1790 $pkg = $cust_bill_pkg->desc;
1791 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1792 ( $sdate, $edate ) = ( '', '' );
1798 ( map { '' } (1..11) ),
1799 ($pkg, $setup, $recur, $sdate, $edate)
1800 ) or die "can't create csv";
1802 $detail .= $csv->string. "\n";
1808 ( $header, $detail );
1814 Pays this invoice with a compliemntary payment. If there is an error,
1815 returns the error, otherwise returns false.
1821 my $cust_pay = new FS::cust_pay ( {
1822 'invnum' => $self->invnum,
1823 'paid' => $self->owed,
1826 'payinfo' => $self->cust_main->payinfo,
1834 Attempts to pay this invoice with a credit card payment via a
1835 Business::OnlinePayment realtime gateway. See
1836 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1837 for supported processors.
1843 $self->realtime_bop( 'CC', @_ );
1848 Attempts to pay this invoice with an electronic check (ACH) payment via a
1849 Business::OnlinePayment realtime gateway. See
1850 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1851 for supported processors.
1857 $self->realtime_bop( 'ECHECK', @_ );
1862 Attempts to pay this invoice with phone bill (LEC) payment via a
1863 Business::OnlinePayment realtime gateway. See
1864 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1865 for supported processors.
1871 $self->realtime_bop( 'LEC', @_ );
1875 my( $self, $method ) = @_;
1877 my $cust_main = $self->cust_main;
1878 my $balance = $cust_main->balance;
1879 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1880 $amount = sprintf("%.2f", $amount);
1881 return "not run (balance $balance)" unless $amount > 0;
1883 my $description = 'Internet Services';
1884 if ( $conf->exists('business-onlinepayment-description') ) {
1885 my $dtempl = $conf->config('business-onlinepayment-description');
1887 my $agent_obj = $cust_main->agent
1888 or die "can't retreive agent for $cust_main (agentnum ".
1889 $cust_main->agentnum. ")";
1890 my $agent = $agent_obj->agent;
1891 my $pkgs = join(', ',
1892 map { $_->part_pkg->pkg }
1893 grep { $_->pkgnum } $self->cust_bill_pkg
1895 $description = eval qq("$dtempl");
1898 $cust_main->realtime_bop($method, $amount,
1899 'description' => $description,
1900 'invnum' => $self->invnum,
1905 =item batch_card OPTION => VALUE...
1907 Adds a payment for this invoice to the pending credit card batch (see
1908 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1909 runs the payment using a realtime gateway.
1914 my ($self, %options) = @_;
1915 my $cust_main = $self->cust_main;
1917 $options{invnum} = $self->invnum;
1919 $cust_main->batch_card(%options);
1922 sub _agent_template {
1924 $self->cust_main->agent_template;
1927 sub _agent_invoice_from {
1929 $self->cust_main->agent_invoice_from;
1932 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1934 Returns an text invoice, as a list of lines.
1936 Options can be passed as a hashref (recommended) or as a list of time, template
1937 and then any key/value pairs for any other options.
1939 I<time>, if specified, is used to control the printing of overdue messages. The
1940 default is now. It isn't the date of the invoice; that's the `_date' field.
1941 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1942 L<Time::Local> and L<Date::Parse> for conversion functions.
1944 I<template>, if specified, is the name of a suffix for alternate invoices.
1946 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1952 my( $today, $template, %opt );
1954 %opt = %{ shift() };
1955 $today = delete($opt{'time'}) || '';
1956 $template = delete($opt{template}) || '';
1958 ( $today, $template, %opt ) = @_;
1961 my %params = ( 'format' => 'template' );
1962 $params{'time'} = $today if $today;
1963 $params{'template'} = $template if $template;
1964 $params{$_} = $opt{$_}
1965 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1967 $self->print_generic( %params );
1970 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1972 Internal method - returns a filename of a filled-in LaTeX template for this
1973 invoice (Note: add ".tex" to get the actual filename), and a filename of
1974 an associated logo (with the .eps extension included).
1976 See print_ps and print_pdf for methods that return PostScript and PDF output.
1978 Options can be passed as a hashref (recommended) or as a list of time, template
1979 and then any key/value pairs for any other options.
1981 I<time>, if specified, is used to control the printing of overdue messages. The
1982 default is now. It isn't the date of the invoice; that's the `_date' field.
1983 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1984 L<Time::Local> and L<Date::Parse> for conversion functions.
1986 I<template>, if specified, is the name of a suffix for alternate invoices.
1988 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1994 my( $today, $template, %opt );
1996 %opt = %{ shift() };
1997 $today = delete($opt{'time'}) || '';
1998 $template = delete($opt{template}) || '';
2000 ( $today, $template, %opt ) = @_;
2003 my %params = ( 'format' => 'latex' );
2004 $params{'time'} = $today if $today;
2005 $params{'template'} = $template if $template;
2006 $params{$_} = $opt{$_}
2007 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2009 $template ||= $self->_agent_template;
2011 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2012 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2016 ) or die "can't open temp file: $!\n";
2018 my $agentnum = $self->cust_main->agentnum;
2020 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2021 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2022 or die "can't write temp file: $!\n";
2024 print $lh $conf->config_binary('logo.eps', $agentnum)
2025 or die "can't write temp file: $!\n";
2028 $params{'logo_file'} = $lh->filename;
2030 my @filled_in = $self->print_generic( %params );
2032 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2036 ) or die "can't open temp file: $!\n";
2037 print $fh join('', @filled_in );
2040 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2041 return ($1, $params{'logo_file'});
2045 =item print_generic OPTION => VALUE ...
2047 Internal method - returns a filled-in template for this invoice as a scalar.
2049 See print_ps and print_pdf for methods that return PostScript and PDF output.
2051 Non optional options include
2052 format - latex, html, template
2054 Optional options include
2056 template - a value used as a suffix for a configuration template
2058 time - a value used to control the printing of overdue messages. The
2059 default is now. It isn't the date of the invoice; that's the `_date' field.
2060 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2061 L<Time::Local> and L<Date::Parse> for conversion functions.
2065 unsquelch_cdr - overrides any per customer cdr squelching when true
2067 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2071 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2072 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2073 # yes: fixed width (dot matrix) text printing will be borked
2076 my( $self, %params ) = @_;
2077 my $today = $params{today} ? $params{today} : time;
2078 warn "$me print_generic called on $self with suffix $params{template}\n"
2081 my $format = $params{format};
2082 die "Unknown format: $format"
2083 unless $format =~ /^(latex|html|template)$/;
2085 my $cust_main = $self->cust_main;
2086 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2087 unless $cust_main->payname
2088 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2090 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2091 'html' => [ '<%=', '%>' ],
2092 'template' => [ '{', '}' ],
2095 #create the template
2096 my $template = $params{template} ? $params{template} : $self->_agent_template;
2097 my $templatefile = "invoice_$format";
2098 $templatefile .= "_$template"
2099 if length($template);
2100 my @invoice_template = map "$_\n", $conf->config($templatefile)
2101 or die "cannot load config data $templatefile";
2104 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2105 #change this to a die when the old code is removed
2106 warn "old-style invoice template $templatefile; ".
2107 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2108 $old_latex = 'true';
2109 @invoice_template = _translate_old_latex_format(@invoice_template);
2112 my $text_template = new Text::Template(
2114 SOURCE => \@invoice_template,
2115 DELIMITERS => $delimiters{$format},
2118 $text_template->compile()
2119 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2122 # additional substitution could possibly cause breakage in existing templates
2123 my %convert_maps = (
2125 'notes' => sub { map "$_", @_ },
2126 'footer' => sub { map "$_", @_ },
2127 'smallfooter' => sub { map "$_", @_ },
2128 'returnaddress' => sub { map "$_", @_ },
2129 'coupon' => sub { map "$_", @_ },
2130 'summary' => sub { map "$_", @_ },
2136 s/%%(.*)$/<!-- $1 -->/g;
2137 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2138 s/\\begin\{enumerate\}/<ol>/g;
2140 s/\\end\{enumerate\}/<\/ol>/g;
2141 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2150 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2152 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2157 s/\\\\\*?\s*$/<BR>/;
2158 s/\\hyphenation\{[\w\s\-]+}//;
2163 'coupon' => sub { "" },
2164 'summary' => sub { "" },
2171 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2172 s/\\begin\{enumerate\}//g;
2174 s/\\end\{enumerate\}//g;
2175 s/\\textbf\{(.*)\}/$1/g;
2182 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2184 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2189 s/\\\\\*?\s*$/\n/; # dubious
2190 s/\\hyphenation\{[\w\s\-]+}//;
2194 'coupon' => sub { "" },
2195 'summary' => sub { "" },
2200 # hashes for differing output formats
2201 my %nbsps = ( 'latex' => '~',
2202 'html' => '', # '&nbps;' would be nice
2203 'template' => '', # not used
2205 my $nbsp = $nbsps{$format};
2207 my %escape_functions = ( 'latex' => \&_latex_escape,
2208 'html' => \&encode_entities,
2209 'template' => sub { shift },
2211 my $escape_function = $escape_functions{$format};
2213 my %date_formats = ( 'latex' => '%b %o, %Y',
2214 'html' => '%b %o, %Y',
2217 my $date_format = $date_formats{$format};
2219 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2221 'html' => sub { return '<b>'. shift(). '</b>'
2223 'template' => sub { shift },
2225 my $embolden_function = $embolden_functions{$format};
2228 # generate template variables
2231 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2235 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2241 $returnaddress = join("\n",
2242 $conf->config_orbase("invoice_${format}returnaddress", $template)
2245 } elsif ( grep /\S/,
2246 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2248 my $convert_map = $convert_maps{$format}{'returnaddress'};
2251 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2256 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2258 my $convert_map = $convert_maps{$format}{'returnaddress'};
2259 $returnaddress = join( "\n", &$convert_map(
2260 map { s/( {2,})/'~' x length($1)/eg;
2264 ( $conf->config('company_name', $self->cust_main->agentnum),
2265 $conf->config('company_address', $self->cust_main->agentnum),
2272 my $warning = "Couldn't find a return address; ".
2273 "do you need to set the company_address configuration value?";
2275 $returnaddress = $nbsp;
2276 #$returnaddress = $warning;
2280 my %invoice_data = (
2283 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2284 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2285 'returnaddress' => $returnaddress,
2286 'agent' => &$escape_function($cust_main->agent->agent),
2289 'invnum' => $self->invnum,
2290 'date' => time2str($date_format, $self->_date),
2291 'today' => time2str('%b %o, %Y', $today),
2292 'terms' => $self->terms,
2293 'template' => $template, #params{'template'},
2294 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2295 'current_charges' => sprintf("%.2f", $self->charged),
2296 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2299 'custnum' => $cust_main->display_custnum,
2300 'agent_custid' => &$escape_function($cust_main->agent_custid),
2301 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2302 payname company address1 address2 city state zip fax
2306 'ship_enable' => $conf->exists('invoice-ship_address'),
2307 'unitprices' => $conf->exists('invoice-unitprice'),
2308 'smallernotes' => $conf->exists('invoice-smallernotes'),
2309 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2311 # better hang on to conf_dir for a while (for old templates)
2312 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2314 #these are only used when doing paged plaintext
2320 $invoice_data{finance_section} = '';
2321 if ( $conf->config('finance_pkgclass') ) {
2323 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2324 $invoice_data{finance_section} = $pkg_class->categoryname;
2326 $invoice_data{finance_amount} = '0.00';
2328 my $countrydefault = $conf->config('countrydefault') || 'US';
2329 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2330 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2331 my $method = $prefix.$_;
2332 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2334 $invoice_data{'ship_country'} = ''
2335 if ( $invoice_data{'ship_country'} eq $countrydefault );
2337 $invoice_data{'cid'} = $params{'cid'}
2340 if ( $cust_main->country eq $countrydefault ) {
2341 $invoice_data{'country'} = '';
2343 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2347 $invoice_data{'address'} = \@address;
2349 $cust_main->payname.
2350 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2351 ? " (P.O. #". $cust_main->payinfo. ")"
2355 push @address, $cust_main->company
2356 if $cust_main->company;
2357 push @address, $cust_main->address1;
2358 push @address, $cust_main->address2
2359 if $cust_main->address2;
2361 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2362 push @address, $invoice_data{'country'}
2363 if $invoice_data{'country'};
2365 while (scalar(@address) < 5);
2367 $invoice_data{'logo_file'} = $params{'logo_file'}
2368 if $params{'logo_file'};
2370 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2371 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2372 #my $balance_due = $self->owed + $pr_total - $cr_total;
2373 my $balance_due = $self->owed + $pr_total;
2374 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2375 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2376 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2377 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2379 my $agentnum = $self->cust_main->agentnum;
2381 my $summarypage = '';
2382 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2385 $invoice_data{'summarypage'} = $summarypage;
2387 #do variable substitution in notes, footer, smallfooter
2388 foreach my $include (qw( notes footer smallfooter coupon )) {
2390 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2393 if ( $conf->exists($inc_file, $agentnum)
2394 && length( $conf->config($inc_file, $agentnum) ) ) {
2396 @inc_src = $conf->config($inc_file, $agentnum);
2400 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2402 my $convert_map = $convert_maps{$format}{$include};
2404 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2405 s/--\@\]/$delimiters{$format}[1]/g;
2408 &$convert_map( $conf->config($inc_file, $agentnum) );
2412 my $inc_tt = new Text::Template (
2414 SOURCE => [ map "$_\n", @inc_src ],
2415 DELIMITERS => $delimiters{$format},
2416 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2418 unless ( $inc_tt->compile() ) {
2419 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2420 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2424 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2426 $invoice_data{$include} =~ s/\n+$//
2427 if ($format eq 'latex');
2430 $invoice_data{'po_line'} =
2431 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2432 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2435 my %money_chars = ( 'latex' => '',
2436 'html' => $conf->config('money_char') || '$',
2439 my $money_char = $money_chars{$format};
2441 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2442 'html' => $conf->config('money_char') || '$',
2445 my $other_money_char = $other_money_chars{$format};
2446 $invoice_data{'dollar'} = $other_money_char;
2448 my @detail_items = ();
2449 my @total_items = ();
2453 $invoice_data{'detail_items'} = \@detail_items;
2454 $invoice_data{'total_items'} = \@total_items;
2455 $invoice_data{'buf'} = \@buf;
2456 $invoice_data{'sections'} = \@sections;
2458 my $previous_section = { 'description' => 'Previous Charges',
2459 'subtotal' => $other_money_char.
2460 sprintf('%.2f', $pr_total),
2461 'summarized' => $summarypage ? 'Y' : '',
2463 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2464 join(' / ', map { $cust_main->balance_date_range(@$_) }
2465 $self->_prior_month30s
2467 if $conf->exists('invoice_include_aging');
2470 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2471 'subtotal' => $taxtotal, # adjusted below
2472 'summarized' => $summarypage ? 'Y' : '',
2474 my $tax_weight = _pkg_category($tax_section->{description})
2475 ? _pkg_category($tax_section->{description})->weight
2477 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2478 $tax_section->{'sort_weight'} = $tax_weight;
2481 my $adjusttotal = 0;
2482 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2483 'subtotal' => 0, # adjusted below
2484 'summarized' => $summarypage ? 'Y' : '',
2486 my $adjust_weight = _pkg_category($adjust_section->{description})
2487 ? _pkg_category($adjust_section->{description})->weight
2489 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2490 $adjust_section->{'sort_weight'} = $adjust_weight;
2492 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2493 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2494 my $late_sections = [];
2495 my $extra_sections = [];
2496 my $extra_lines = ();
2497 if ( $multisection ) {
2498 ($extra_sections, $extra_lines) =
2499 $self->_items_extra_usage_sections($escape_function, $format)
2500 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2502 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2504 push @detail_items, @$extra_lines if $extra_lines;
2506 $self->_items_sections( $late_sections, # this could stand a refactor
2512 if ($conf->exists('svc_phone_sections')) {
2513 my ($phone_sections, $phone_lines) =
2514 $self->_items_svc_phone_sections($escape_function, $format);
2515 push @{$late_sections}, @$phone_sections;
2516 push @detail_items, @$phone_lines;
2519 push @sections, { 'description' => '', 'subtotal' => '' };
2522 unless ( $conf->exists('disable_previous_balance')
2523 || $conf->exists('previous_balance-summary_only')
2527 foreach my $line_item ( $self->_items_previous ) {
2530 ext_description => [],
2532 $detail->{'ref'} = $line_item->{'pkgnum'};
2533 $detail->{'quantity'} = 1;
2534 $detail->{'section'} = $previous_section;
2535 $detail->{'description'} = &$escape_function($line_item->{'description'});
2536 if ( exists $line_item->{'ext_description'} ) {
2537 @{$detail->{'ext_description'}} = map {
2538 &$escape_function($_);
2539 } @{$line_item->{'ext_description'}};
2541 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2542 $line_item->{'amount'};
2543 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2545 push @detail_items, $detail;
2546 push @buf, [ $detail->{'description'},
2547 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2553 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2554 push @buf, ['','-----------'];
2555 push @buf, [ 'Total Previous Balance',
2556 $money_char. sprintf("%10.2f", $pr_total) ];
2560 foreach my $section (@sections, @$late_sections) {
2562 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2563 if ( $invoice_data{finance_section} &&
2564 $section->{'description'} eq $invoice_data{finance_section} );
2566 $section->{'subtotal'} = $other_money_char.
2567 sprintf('%.2f', $section->{'subtotal'})
2570 # begin some normalization
2571 $section->{'amount'} = $section->{'subtotal'}
2575 if ( $section->{'description'} ) {
2576 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2581 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2583 $options{'section'} = $section if $multisection;
2584 $options{'format'} = $format;
2585 $options{'escape_function'} = $escape_function;
2586 $options{'format_function'} = sub { () } unless $unsquelched;
2587 $options{'unsquelched'} = $unsquelched;
2588 $options{'summary_page'} = $summarypage;
2589 $options{'skip_usage'} =
2590 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2591 $options{'multilocation'} = $multilocation;
2593 foreach my $line_item ( $self->_items_pkg(%options) ) {
2595 ext_description => [],
2597 $detail->{'ref'} = $line_item->{'pkgnum'};
2598 $detail->{'quantity'} = $line_item->{'quantity'};
2599 $detail->{'section'} = $section;
2600 $detail->{'description'} = &$escape_function($line_item->{'description'});
2601 if ( exists $line_item->{'ext_description'} ) {
2602 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2604 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2605 $line_item->{'amount'};
2606 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2607 $line_item->{'unit_amount'};
2608 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2610 push @detail_items, $detail;
2611 push @buf, ( [ $detail->{'description'},
2612 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2614 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2618 if ( $section->{'description'} ) {
2619 push @buf, ( ['','-----------'],
2620 [ $section->{'description'}. ' sub-total',
2621 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2630 $invoice_data{current_less_finance} =
2631 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2633 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2634 unshift @sections, $previous_section if $pr_total;
2637 foreach my $tax ( $self->_items_tax ) {
2639 $taxtotal += $tax->{'amount'};
2641 my $description = &$escape_function( $tax->{'description'} );
2642 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2644 if ( $multisection ) {
2646 my $money = $old_latex ? '' : $money_char;
2647 push @detail_items, {
2648 ext_description => [],
2651 description => $description,
2652 amount => $money. $amount,
2654 section => $tax_section,
2659 push @total_items, {
2660 'total_item' => $description,
2661 'total_amount' => $other_money_char. $amount,
2666 push @buf,[ $description,
2667 $money_char. $amount,
2674 $total->{'total_item'} = 'Sub-total';
2675 $total->{'total_amount'} =
2676 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2678 if ( $multisection ) {
2679 $tax_section->{'subtotal'} = $other_money_char.
2680 sprintf('%.2f', $taxtotal);
2681 $tax_section->{'pretotal'} = 'New charges sub-total '.
2682 $total->{'total_amount'};
2683 push @sections, $tax_section if $taxtotal;
2685 unshift @total_items, $total;
2688 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2690 push @buf,['','-----------'];
2691 push @buf,[( $conf->exists('disable_previous_balance')
2693 : 'Total New Charges'
2695 $money_char. sprintf("%10.2f",$self->charged) ];
2700 $total->{'total_item'} = &$embolden_function('Total');
2701 $total->{'total_amount'} =
2702 &$embolden_function(
2705 $self->charged + ( $conf->exists('disable_previous_balance')
2711 if ( $multisection ) {
2712 if ( $adjust_section->{'sort_weight'} ) {
2713 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2714 sprintf("%.2f", ($self->billing_balance || 0) );
2716 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2717 sprintf('%.2f', $self->charged );
2720 push @total_items, $total;
2722 push @buf,['','-----------'];
2723 push @buf,['Total Charges',
2725 sprintf( '%10.2f', $self->charged +
2726 ( $conf->exists('disable_previous_balance')
2735 unless ( $conf->exists('disable_previous_balance') ) {
2736 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2739 my $credittotal = 0;
2740 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2743 $total->{'total_item'} = &$escape_function($credit->{'description'});
2744 $credittotal += $credit->{'amount'};
2745 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2746 $adjusttotal += $credit->{'amount'};
2747 if ( $multisection ) {
2748 my $money = $old_latex ? '' : $money_char;
2749 push @detail_items, {
2750 ext_description => [],
2753 description => &$escape_function($credit->{'description'}),
2754 amount => $money. $credit->{'amount'},
2756 section => $adjust_section,
2759 push @total_items, $total;
2763 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2766 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2767 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2771 my $paymenttotal = 0;
2772 foreach my $payment ( $self->_items_payments ) {
2774 $total->{'total_item'} = &$escape_function($payment->{'description'});
2775 $paymenttotal += $payment->{'amount'};
2776 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2777 $adjusttotal += $payment->{'amount'};
2778 if ( $multisection ) {
2779 my $money = $old_latex ? '' : $money_char;
2780 push @detail_items, {
2781 ext_description => [],
2784 description => &$escape_function($payment->{'description'}),
2785 amount => $money. $payment->{'amount'},
2787 section => $adjust_section,
2790 push @total_items, $total;
2792 push @buf, [ $payment->{'description'},
2793 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2796 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2798 if ( $multisection ) {
2799 $adjust_section->{'subtotal'} = $other_money_char.
2800 sprintf('%.2f', $adjusttotal);
2801 push @sections, $adjust_section
2802 unless $adjust_section->{sort_weight};
2807 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2808 $total->{'total_amount'} =
2809 &$embolden_function(
2810 $other_money_char. sprintf('%.2f', $summarypage
2812 $self->billing_balance
2813 : $self->owed + $pr_total
2816 if ( $multisection && !$adjust_section->{sort_weight} ) {
2817 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2818 $total->{'total_amount'};
2820 push @total_items, $total;
2822 push @buf,['','-----------'];
2823 push @buf,[$self->balance_due_msg, $money_char.
2824 sprintf("%10.2f", $balance_due ) ];
2828 if ( $multisection ) {
2829 if ($conf->exists('svc_phone_sections')) {
2831 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2832 $total->{'total_amount'} =
2833 &$embolden_function(
2834 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2836 my $last_section = pop @sections;
2837 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2838 $total->{'total_amount'};
2839 push @sections, $last_section;
2841 push @sections, @$late_sections
2845 my @includelist = ();
2846 push @includelist, 'summary' if $summarypage;
2847 foreach my $include ( @includelist ) {
2849 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2852 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2854 @inc_src = $conf->config($inc_file, $agentnum);
2858 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2860 my $convert_map = $convert_maps{$format}{$include};
2862 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2863 s/--\@\]/$delimiters{$format}[1]/g;
2866 &$convert_map( $conf->config($inc_file, $agentnum) );
2870 my $inc_tt = new Text::Template (
2872 SOURCE => [ map "$_\n", @inc_src ],
2873 DELIMITERS => $delimiters{$format},
2874 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2876 unless ( $inc_tt->compile() ) {
2877 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2878 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2882 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2884 $invoice_data{$include} =~ s/\n+$//
2885 if ($format eq 'latex');
2890 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2891 /invoice_lines\((\d*)\)/;
2892 $invoice_lines += $1 || scalar(@buf);
2895 die "no invoice_lines() functions in template?"
2896 if ( $format eq 'template' && !$wasfunc );
2898 if ($format eq 'template') {
2900 if ( $invoice_lines ) {
2901 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2902 $invoice_data{'total_pages'}++
2903 if scalar(@buf) % $invoice_lines;
2906 #setup subroutine for the template
2907 sub FS::cust_bill::_template::invoice_lines {
2908 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2910 scalar(@FS::cust_bill::_template::buf)
2911 ? shift @FS::cust_bill::_template::buf
2920 push @collect, split("\n",
2921 $text_template->fill_in( HASH => \%invoice_data,
2922 PACKAGE => 'FS::cust_bill::_template'
2925 $FS::cust_bill::_template::page++;
2927 map "$_\n", @collect;
2929 warn "filling in template for invoice ". $self->invnum. "\n"
2931 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2934 $text_template->fill_in(HASH => \%invoice_data);
2938 # helper routine for generating date ranges
2939 sub _prior_month30s {
2942 [ 1, 2592000 ], # 0-30 days ago
2943 [ 2592000, 5184000 ], # 30-60 days ago
2944 [ 5184000, 7776000 ], # 60-90 days ago
2945 [ 7776000, 0 ], # 90+ days ago
2948 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2949 $_->[1] ? $self->_date - $_->[1] - 1 : '',
2954 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2956 Returns an postscript invoice, as a scalar.
2958 Options can be passed as a hashref (recommended) or as a list of time, template
2959 and then any key/value pairs for any other options.
2961 I<time> an optional value used to control the printing of overdue messages. The
2962 default is now. It isn't the date of the invoice; that's the `_date' field.
2963 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2964 L<Time::Local> and L<Date::Parse> for conversion functions.
2966 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2973 my ($file, $lfile) = $self->print_latex(@_);
2974 my $ps = generate_ps($file);
2980 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2982 Returns an PDF invoice, as a scalar.
2984 Options can be passed as a hashref (recommended) or as a list of time, template
2985 and then any key/value pairs for any other options.
2987 I<time> an optional value used to control the printing of overdue messages. The
2988 default is now. It isn't the date of the invoice; that's the `_date' field.
2989 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2990 L<Time::Local> and L<Date::Parse> for conversion functions.
2992 I<template>, if specified, is the name of a suffix for alternate invoices.
2994 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3001 my ($file, $lfile) = $self->print_latex(@_);
3002 my $pdf = generate_pdf($file);
3008 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3010 Returns an HTML invoice, as a scalar.
3012 I<time> an optional value used to control the printing of overdue messages. The
3013 default is now. It isn't the date of the invoice; that's the `_date' field.
3014 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3015 L<Time::Local> and L<Date::Parse> for conversion functions.
3017 I<template>, if specified, is the name of a suffix for alternate invoices.
3019 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3021 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3022 when emailing the invoice as part of a multipart/related MIME email.
3030 %params = %{ shift() };
3032 $params{'time'} = shift;
3033 $params{'template'} = shift;
3034 $params{'cid'} = shift;
3037 $params{'format'} = 'html';
3039 $self->print_generic( %params );
3042 # quick subroutine for print_latex
3044 # There are ten characters that LaTeX treats as special characters, which
3045 # means that they do not simply typeset themselves:
3046 # # $ % & ~ _ ^ \ { }
3048 # TeX ignores blanks following an escaped character; if you want a blank (as
3049 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3053 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3054 $value =~ s/([<>])/\$$1\$/g;
3058 #utility methods for print_*
3060 sub _translate_old_latex_format {
3061 warn "_translate_old_latex_format called\n"
3068 if ( $line =~ /^%%Detail\s*$/ ) {
3070 push @template, q![@--!,
3071 q! foreach my $_tr_line (@detail_items) {!,
3072 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3073 q! $_tr_line->{'description'} .= !,
3074 q! "\\tabularnewline\n~~".!,
3075 q! join( "\\tabularnewline\n~~",!,
3076 q! @{$_tr_line->{'ext_description'}}!,
3080 while ( ( my $line_item_line = shift )
3081 !~ /^%%EndDetail\s*$/ ) {
3082 $line_item_line =~ s/'/\\'/g; # nice LTS
3083 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3084 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3085 push @template, " \$OUT .= '$line_item_line';";
3088 push @template, '}',
3091 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3093 push @template, '[@--',
3094 ' foreach my $_tr_line (@total_items) {';
3096 while ( ( my $total_item_line = shift )
3097 !~ /^%%EndTotalDetails\s*$/ ) {
3098 $total_item_line =~ s/'/\\'/g; # nice LTS
3099 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3100 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3101 push @template, " \$OUT .= '$total_item_line';";
3104 push @template, '}',
3108 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3109 push @template, $line;
3115 warn "$_\n" foreach @template;
3124 #check for an invoice-specific override
3125 return $self->invoice_terms if $self->invoice_terms;
3127 #check for a customer- specific override
3128 my $cust_main = $self->cust_main;
3129 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3131 #use configured default
3132 $conf->config('invoice_default_terms') || '';
3138 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3139 $duedate = $self->_date() + ( $1 * 86400 );
3146 $self->due_date ? time2str(shift, $self->due_date) : '';
3149 sub balance_due_msg {
3151 my $msg = 'Balance Due';
3152 return $msg unless $self->terms;
3153 if ( $self->due_date ) {
3154 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3155 } elsif ( $self->terms ) {
3156 $msg .= ' - '. $self->terms;
3161 sub balance_due_date {
3164 if ( $conf->exists('invoice_default_terms')
3165 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3166 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3171 =item invnum_date_pretty
3173 Returns a string with the invoice number and date, for example:
3174 "Invoice #54 (3/20/2008)"
3178 sub invnum_date_pretty {
3180 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3185 Returns a string with the date, for example: "3/20/2008"
3191 time2str('%x', $self->_date);
3194 use vars qw(%pkg_category_cache);
3195 sub _items_sections {
3198 my $summarypage = shift;
3200 my $extra_sections = shift;
3204 my %late_subtotal = ();
3207 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3210 my $usage = $cust_bill_pkg->usage;
3212 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3213 next if ( $display->summary && $summarypage );
3215 my $section = $display->section;
3216 my $type = $display->type;
3218 $not_tax{$section} = 1
3219 unless $cust_bill_pkg->pkgnum == 0;
3221 if ( $display->post_total && !$summarypage ) {
3222 if (! $type || $type eq 'S') {
3223 $late_subtotal{$section} += $cust_bill_pkg->setup
3224 if $cust_bill_pkg->setup != 0;
3228 $late_subtotal{$section} += $cust_bill_pkg->recur
3229 if $cust_bill_pkg->recur != 0;
3232 if ($type && $type eq 'R') {
3233 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3234 if $cust_bill_pkg->recur != 0;
3237 if ($type && $type eq 'U') {
3238 $late_subtotal{$section} += $usage
3239 unless scalar(@$extra_sections);
3244 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3246 if (! $type || $type eq 'S') {
3247 $subtotal{$section} += $cust_bill_pkg->setup
3248 if $cust_bill_pkg->setup != 0;
3252 $subtotal{$section} += $cust_bill_pkg->recur
3253 if $cust_bill_pkg->recur != 0;
3256 if ($type && $type eq 'R') {
3257 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3258 if $cust_bill_pkg->recur != 0;
3261 if ($type && $type eq 'U') {
3262 $subtotal{$section} += $usage
3263 unless scalar(@$extra_sections);
3272 %pkg_category_cache = ();
3274 push @$late, map { { 'description' => &{$escape}($_),
3275 'subtotal' => $late_subtotal{$_},
3277 'sort_weight' => ( _pkg_category($_)
3278 ? _pkg_category($_)->weight
3281 ((_pkg_category($_) && _pkg_category($_)->condense)
3282 ? $self->_condense_section($format)
3286 sort _sectionsort keys %late_subtotal;
3289 if ( $summarypage ) {
3290 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3291 map { $_->categoryname } qsearch('pkg_category', {});
3293 @sections = keys %subtotal;
3296 my @early = map { { 'description' => &{$escape}($_),
3297 'subtotal' => $subtotal{$_},
3298 'summarized' => $not_tax{$_} ? '' : 'Y',
3299 'tax_section' => $not_tax{$_} ? '' : 'Y',
3300 'sort_weight' => ( _pkg_category($_)
3301 ? _pkg_category($_)->weight
3304 ((_pkg_category($_) && _pkg_category($_)->condense)
3305 ? $self->_condense_section($format)
3310 push @early, @$extra_sections if $extra_sections;
3312 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3316 #helper subs for above
3319 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3323 my $categoryname = shift;
3324 $pkg_category_cache{$categoryname} ||=
3325 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3328 my %condensed_format = (
3329 'label' => [ qw( Description Qty Amount ) ],
3331 sub { shift->{description} },
3332 sub { shift->{quantity} },
3333 sub { shift->{amount} },
3335 'align' => [ qw( l r r ) ],
3336 'span' => [ qw( 5 1 1 ) ], # unitprices?
3337 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3340 sub _condense_section {
3341 my ( $self, $format ) = ( shift, shift );
3343 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3344 qw( description_generator
3347 total_line_generator
3352 sub _condensed_generator_defaults {
3353 my ( $self, $format ) = ( shift, shift );
3354 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3363 sub _condensed_header_generator {
3364 my ( $self, $format ) = ( shift, shift );
3366 my ( $f, $prefix, $suffix, $separator, $column ) =
3367 _condensed_generator_defaults($format);
3369 if ($format eq 'latex') {
3370 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3371 $suffix = "\\\\\n\\hline";
3374 sub { my ($d,$a,$s,$w) = @_;
3375 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3377 } elsif ( $format eq 'html' ) {
3378 $prefix = '<th></th>';
3382 sub { my ($d,$a,$s,$w) = @_;
3383 return qq!<th align="$html_align{$a}">$d</th>!;
3391 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3393 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3396 $prefix. join($separator, @result). $suffix;
3401 sub _condensed_description_generator {
3402 my ( $self, $format ) = ( shift, shift );
3404 my ( $f, $prefix, $suffix, $separator, $column ) =
3405 _condensed_generator_defaults($format);
3407 if ($format eq 'latex') {
3408 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3410 $separator = " & \n";
3412 sub { my ($d,$a,$s,$w) = @_;
3413 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3415 }elsif ( $format eq 'html' ) {
3416 $prefix = '"><td align="center"></td>';
3420 sub { my ($d,$a,$s,$w) = @_;
3421 return qq!<td align="$html_align{$a}">$d</td>!;
3429 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3430 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3431 map { $f->{$_}->[$i] } qw(align span width)
3435 $prefix. join( $separator, @result ). $suffix;
3440 sub _condensed_total_generator {
3441 my ( $self, $format ) = ( shift, shift );
3443 my ( $f, $prefix, $suffix, $separator, $column ) =
3444 _condensed_generator_defaults($format);
3447 if ($format eq 'latex') {
3450 $separator = " & \n";
3452 sub { my ($d,$a,$s,$w) = @_;
3453 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3455 }elsif ( $format eq 'html' ) {
3459 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3461 sub { my ($d,$a,$s,$w) = @_;
3462 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3471 # my $r = &{$f->{fields}->[$i]}(@args);
3472 # $r .= ' Total' unless $i;
3474 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3476 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3477 map { $f->{$_}->[$i] } qw(align span width)
3481 $prefix. join( $separator, @result ). $suffix;
3486 =item total_line_generator FORMAT
3488 Returns a coderef used for generation of invoice total line items for this
3489 usage_class. FORMAT is either html or latex
3493 # should not be used: will have issues with hash element names (description vs
3494 # total_item and amount vs total_amount -- another array of functions?
3496 sub _condensed_total_line_generator {
3497 my ( $self, $format ) = ( shift, shift );
3499 my ( $f, $prefix, $suffix, $separator, $column ) =
3500 _condensed_generator_defaults($format);
3503 if ($format eq 'latex') {
3506 $separator = " & \n";
3508 sub { my ($d,$a,$s,$w) = @_;
3509 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3511 }elsif ( $format eq 'html' ) {
3515 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3517 sub { my ($d,$a,$s,$w) = @_;
3518 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3527 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3529 &{$column}( &{$f->{fields}->[$i]}(@args),
3530 map { $f->{$_}->[$i] } qw(align span width)
3534 $prefix. join( $separator, @result ). $suffix;
3539 #sub _items_extra_usage_sections {
3541 # my $escape = shift;
3543 # my %sections = ();
3545 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3546 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3548 # next unless $cust_bill_pkg->pkgnum > 0;
3550 # foreach my $section ( keys %usage_class ) {
3552 # my $usage = $cust_bill_pkg->usage($section);
3554 # next unless $usage && $usage > 0;
3556 # $sections{$section} ||= 0;
3557 # $sections{$section} += $usage;
3563 # map { { 'description' => &{$escape}($_),
3564 # 'subtotal' => $sections{$_},
3565 # 'summarized' => '',
3566 # 'tax_section' => '',
3569 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3573 sub _items_extra_usage_sections {
3582 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3583 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3584 next unless $cust_bill_pkg->pkgnum > 0;
3586 foreach my $classnum ( keys %usage_class ) {
3587 my $section = $usage_class{$classnum}->classname;
3588 $classnums{$section} = $classnum;
3590 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3591 my $amount = $detail->amount;
3592 next unless $amount && $amount > 0;
3594 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3595 $sections{$section}{amount} += $amount; #subtotal
3596 $sections{$section}{calls}++;
3597 $sections{$section}{duration} += $detail->duration;
3599 my $desc = $detail->regionname;
3600 my $description = $desc;
3601 $description = substr($desc, 0, 50). '...'
3602 if $format eq 'latex' && length($desc) > 50;
3604 $lines{$section}{$desc} ||= {
3605 description => &{$escape}($description),
3606 #pkgpart => $part_pkg->pkgpart,
3607 pkgnum => $cust_bill_pkg->pkgnum,
3612 #unit_amount => $cust_bill_pkg->unitrecur,
3613 quantity => $cust_bill_pkg->quantity,
3614 product_code => 'N/A',
3615 ext_description => [],
3618 $lines{$section}{$desc}{amount} += $amount;
3619 $lines{$section}{$desc}{calls}++;
3620 $lines{$section}{$desc}{duration} += $detail->duration;
3626 my %sectionmap = ();
3627 foreach (keys %sections) {
3628 my $usage_class = $usage_class{$classnums{$_}};
3629 $sectionmap{$_} = { 'description' => &{$escape}($_),
3630 'amount' => $sections{$_}{amount}, #subtotal
3631 'calls' => $sections{$_}{calls},
3632 'duration' => $sections{$_}{duration},
3634 'tax_section' => '',
3635 'sort_weight' => $usage_class->weight,
3636 ( $usage_class->format
3637 ? ( map { $_ => $usage_class->$_($format) }
3638 qw( description_generator header_generator total_generator total_line_generator )
3645 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3649 foreach my $section ( keys %lines ) {
3650 foreach my $line ( keys %{$lines{$section}} ) {
3651 my $l = $lines{$section}{$line};
3652 $l->{section} = $sectionmap{$section};
3653 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3654 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3659 return(\@sections, \@lines);
3663 sub _items_svc_phone_sections {
3672 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3674 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3675 next unless $cust_bill_pkg->pkgnum > 0;
3677 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3679 my $phonenum = $detail->phonenum;
3680 next unless $phonenum;
3682 my $amount = $detail->amount;
3683 next unless $amount && $amount > 0;
3685 $sections{$phonenum} ||= { 'amount' => 0,
3688 'sort_weight' => -1,
3689 'phonenum' => $phonenum,
3691 $sections{$phonenum}{amount} += $amount; #subtotal
3692 $sections{$phonenum}{calls}++;
3693 $sections{$phonenum}{duration} += $detail->duration;
3695 my $desc = $detail->regionname;
3696 my $description = $desc;
3697 $description = substr($desc, 0, 50). '...'
3698 if $format eq 'latex' && length($desc) > 50;
3700 $lines{$phonenum}{$desc} ||= {
3701 description => &{$escape}($description),
3702 #pkgpart => $part_pkg->pkgpart,
3710 product_code => 'N/A',
3711 ext_description => [],
3714 $lines{$phonenum}{$desc}{amount} += $amount;
3715 $lines{$phonenum}{$desc}{calls}++;
3716 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3718 my $line = $usage_class{$detail->classnum}->classname;
3719 $sections{"$phonenum $line"} ||=
3723 'sort_weight' => $usage_class{$detail->classnum}->weight,
3724 'phonenum' => $phonenum,
3726 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3727 $sections{"$phonenum $line"}{calls}++;
3728 $sections{"$phonenum $line"}{duration} += $detail->duration;
3730 $lines{"$phonenum $line"}{$desc} ||= {
3731 description => &{$escape}($description),
3732 #pkgpart => $part_pkg->pkgpart,
3740 product_code => 'N/A',
3741 ext_description => [],
3744 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3745 $lines{"$phonenum $line"}{$desc}{calls}++;
3746 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3747 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3748 $detail->formatted('format' => $format);
3753 my %sectionmap = ();
3754 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3755 my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3756 foreach ( keys %sections ) {
3757 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3758 my $usage_class = $summary ? $simple : $usage_simple;
3759 my $ending = $summary ? ' usage charges' : '';
3760 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3761 'amount' => $sections{$_}{amount}, #subtotal
3762 'calls' => $sections{$_}{calls},
3763 'duration' => $sections{$_}{duration},
3765 'tax_section' => '',
3766 'phonenum' => $sections{$_}{phonenum},
3767 'sort_weight' => $sections{$_}{sort_weight},
3768 'post_total' => $summary, #inspire pagebreak
3770 ( map { $_ => $usage_class->$_($format) }
3771 qw( description_generator
3774 total_line_generator
3781 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3782 $a->{sort_weight} <=> $b->{sort_weight}
3787 foreach my $section ( keys %lines ) {
3788 foreach my $line ( keys %{$lines{$section}} ) {
3789 my $l = $lines{$section}{$line};
3790 $l->{section} = $sectionmap{$section};
3791 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3792 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3797 return(\@sections, \@lines);
3804 #my @display = scalar(@_)
3806 # : qw( _items_previous _items_pkg );
3807 # #: qw( _items_pkg );
3808 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3809 my @display = qw( _items_previous _items_pkg );
3812 foreach my $display ( @display ) {
3813 push @b, $self->$display(@_);
3818 sub _items_previous {
3820 my $cust_main = $self->cust_main;
3821 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3823 foreach ( @pr_cust_bill ) {
3824 my $date = $conf->exists('invoice_show_prior_due_date')
3825 ? 'due '. $_->due_date2str($date_format)
3826 : time2str('%x', $_->_date); # date_format here, too,
3827 # but fix _items_cust_bill_pkg,
3830 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3831 #'pkgpart' => 'N/A',
3833 'amount' => sprintf("%.2f", $_->owed),
3839 # 'description' => 'Previous Balance',
3840 # #'pkgpart' => 'N/A',
3841 # 'pkgnum' => 'N/A',
3842 # 'amount' => sprintf("%10.2f", $pr_total ),
3843 # 'ext_description' => [ map {
3844 # "Invoice ". $_->invnum.
3845 # " (". time2str("%x",$_->_date). ") ".
3846 # sprintf("%10.2f", $_->owed)
3847 # } @pr_cust_bill ],
3855 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3856 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3857 if ($options{section} && $options{section}->{condensed}) {
3859 local $Storable::canonical = 1;
3860 foreach ( @items ) {
3862 delete $item->{ref};
3863 delete $item->{ext_description};
3864 my $key = freeze($item);
3865 $itemshash{$key} ||= 0;
3866 $itemshash{$key} ++; # += $item->{quantity};
3868 @items = sort { $a->{description} cmp $b->{description} }
3869 map { my $i = thaw($_);
3870 $i->{quantity} = $itemshash{$_};
3872 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3881 return 0 unless $a cmp $b;
3882 return -1 if $b eq 'Tax';
3883 return 1 if $a eq 'Tax';
3884 return -1 if $b eq 'Other surcharges';
3885 return 1 if $a eq 'Other surcharges';
3891 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3892 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3895 sub _items_cust_bill_pkg {
3897 my $cust_bill_pkg = shift;
3900 my $format = $opt{format} || '';
3901 my $escape_function = $opt{escape_function} || sub { shift };
3902 my $format_function = $opt{format_function} || '';
3903 my $unsquelched = $opt{unsquelched} || '';
3904 my $section = $opt{section}->{description} if $opt{section};
3905 my $summary_page = $opt{summary_page} || '';
3906 my $multilocation = $opt{multilocation} || '';
3909 my ($s, $r, $u) = ( undef, undef, undef );
3910 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3913 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3914 if ( $_ && !$cust_bill_pkg->hidden ) {
3915 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3916 $_->{amount} =~ s/^\-0\.00$/0.00/;
3917 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3919 unless $_->{amount} == 0;
3924 foreach my $display ( grep { defined($section)
3925 ? $_->section eq $section
3928 grep { !$_->summary || !$summary_page }
3929 $cust_bill_pkg->cust_bill_pkg_display
3933 my $type = $display->type;
3935 my $desc = $cust_bill_pkg->desc;
3936 $desc = substr($desc, 0, 50). '...'
3937 if $format eq 'latex' && length($desc) > 50;
3939 my %details_opt = ( 'format' => $format,
3940 'escape_function' => $escape_function,
3941 'format_function' => $format_function,
3944 if ( $cust_bill_pkg->pkgnum > 0 ) {
3946 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3948 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3950 my $description = $desc;
3951 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3954 unless ( $cust_pkg->part_pkg->hide_svc_detail
3955 || $cust_bill_pkg->hidden )
3957 push @d, map &{$escape_function}($_),
3958 $cust_pkg->h_labels_short($self->_date);
3959 if ( $multilocation ) {
3960 my $loc = $cust_pkg->location_label;
3961 $loc = substr($desc, 0, 50). '...'
3962 if $format eq 'latex' && length($loc) > 50;
3963 push @d, &{$escape_function}($loc);
3966 push @d, $cust_bill_pkg->details(%details_opt)
3967 if $cust_bill_pkg->recur == 0;
3969 if ( $cust_bill_pkg->hidden ) {
3970 $s->{amount} += $cust_bill_pkg->setup;
3971 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3972 push @{ $s->{ext_description} }, @d;
3975 description => $description,
3976 #pkgpart => $part_pkg->pkgpart,
3977 pkgnum => $cust_bill_pkg->pkgnum,
3978 amount => $cust_bill_pkg->setup,
3979 unit_amount => $cust_bill_pkg->unitsetup,
3980 quantity => $cust_bill_pkg->quantity,
3981 ext_description => \@d,
3987 if ( $cust_bill_pkg->recur != 0 &&
3988 ( !$type || $type eq 'R' || $type eq 'U' )
3992 my $is_summary = $display->summary;
3993 my $description = ($is_summary && $type && $type eq 'U')
3994 ? "Usage charges" : $desc;
3996 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3997 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3998 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
4003 #at least until cust_bill_pkg has "past" ranges in addition to
4004 #the "future" sdate/edate ones... see #3032
4005 my @dates = ( $self->_date );
4006 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4007 push @dates, $prev->sdate if $prev;
4009 unless ( $cust_pkg->part_pkg->hide_svc_detail
4010 || $cust_bill_pkg->itemdesc
4011 || $cust_bill_pkg->hidden
4012 || $is_summary && $type && $type eq 'U' )
4014 push @d, map &{$escape_function}($_),
4015 $cust_pkg->h_labels_short(@dates)
4016 #$cust_bill_pkg->edate,
4017 #$cust_bill_pkg->sdate)
4019 if ( $multilocation ) {
4020 my $loc = $cust_pkg->location_label;
4021 $loc = substr($desc, 0, 50). '...'
4022 if $format eq 'latex' && length($loc) > 50;
4023 push @d, &{$escape_function}($loc);
4027 push @d, $cust_bill_pkg->details(%details_opt)
4028 unless ($is_summary || $type && $type eq 'R');
4032 $amount = $cust_bill_pkg->recur;
4033 }elsif($type eq 'R') {
4034 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4035 }elsif($type eq 'U') {
4036 $amount = $cust_bill_pkg->usage;
4039 if ( !$type || $type eq 'R' ) {
4041 if ( $cust_bill_pkg->hidden ) {
4042 $r->{amount} += $amount;
4043 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4044 push @{ $r->{ext_description} }, @d;
4047 description => $description,
4048 #pkgpart => $part_pkg->pkgpart,
4049 pkgnum => $cust_bill_pkg->pkgnum,
4051 unit_amount => $cust_bill_pkg->unitrecur,
4052 quantity => $cust_bill_pkg->quantity,
4053 ext_description => \@d,
4057 } elsif ( $amount ) { # && $type eq 'U'
4059 if ( $cust_bill_pkg->hidden ) {
4060 $u->{amount} += $amount;
4061 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4062 push @{ $u->{ext_description} }, @d;
4065 description => $description,
4066 #pkgpart => $part_pkg->pkgpart,
4067 pkgnum => $cust_bill_pkg->pkgnum,
4069 unit_amount => $cust_bill_pkg->unitrecur,
4070 quantity => $cust_bill_pkg->quantity,
4071 ext_description => \@d,
4077 } # recurring or usage with recurring charge
4079 } else { #pkgnum tax or one-shot line item (??)
4081 if ( $cust_bill_pkg->setup != 0 ) {
4083 'description' => $desc,
4084 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4087 if ( $cust_bill_pkg->recur != 0 ) {
4089 'description' => "$desc (".
4090 time2str("%x", $cust_bill_pkg->sdate). ' - '.
4091 time2str("%x", $cust_bill_pkg->edate). ')',
4092 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4102 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4104 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4105 $_->{amount} =~ s/^\-0\.00$/0.00/;
4106 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4108 unless $_->{amount} == 0;
4116 sub _items_credits {
4117 my( $self, %opt ) = @_;
4118 my $trim_len = $opt{'trim_len'} || 60;
4122 foreach ( $self->cust_credited ) {
4124 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4126 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4127 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4128 $reason = " ($reason) " if $reason;
4131 #'description' => 'Credit ref\#'. $_->crednum.
4132 # " (". time2str("%x",$_->cust_credit->_date) .")".
4134 'description' => 'Credit applied '.
4135 time2str("%x",$_->cust_credit->_date). $reason,
4136 'amount' => sprintf("%.2f",$_->amount),
4144 sub _items_payments {
4148 #get & print payments
4149 foreach ( $self->cust_bill_pay ) {
4151 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4154 'description' => "Payment received ".
4155 time2str("%x",$_->cust_pay->_date ),
4156 'amount' => sprintf("%.2f", $_->amount )
4164 =item call_details [ OPTION => VALUE ... ]
4166 Returns an array of CSV strings representing the call details for this invoice
4167 The only option available is the boolean prepend_billed_number
4172 my ($self, %opt) = @_;
4174 my $format_function = sub { shift };
4176 if ($opt{prepend_billed_number}) {
4177 $format_function = sub {
4181 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4186 my @details = map { $_->details( 'format_function' => $format_function,
4187 'escape_function' => sub{ return() },
4191 $self->cust_bill_pkg;
4192 my $header = $details[0];
4193 ( $header, grep { $_ ne $header } @details );
4203 =item process_reprint
4207 sub process_reprint {
4208 process_re_X('print', @_);
4211 =item process_reemail
4215 sub process_reemail {
4216 process_re_X('email', @_);
4224 process_re_X('fax', @_);
4232 process_re_X('ftp', @_);
4239 sub process_respool {
4240 process_re_X('spool', @_);
4243 use Storable qw(thaw);
4247 my( $method, $job ) = ( shift, shift );
4248 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4250 my $param = thaw(decode_base64(shift));
4251 warn Dumper($param) if $DEBUG;
4262 my($method, $job, %param ) = @_;
4264 warn "re_X $method for job $job with param:\n".
4265 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4268 #some false laziness w/search/cust_bill.html
4270 my $orderby = 'ORDER BY cust_bill._date';
4272 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4274 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4276 my @cust_bill = qsearch( {
4277 #'select' => "cust_bill.*",
4278 'table' => 'cust_bill',
4279 'addl_from' => $addl_from,
4281 'extra_sql' => $extra_sql,
4282 'order_by' => $orderby,
4286 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4288 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4291 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4292 foreach my $cust_bill ( @cust_bill ) {
4293 $cust_bill->$method();
4295 if ( $job ) { #progressbar foo
4297 if ( time - $min_sec > $last ) {
4298 my $error = $job->update_statustext(
4299 int( 100 * $num / scalar(@cust_bill) )
4301 die $error if $error;
4312 =head1 CLASS METHODS
4318 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4324 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4329 Returns an SQL fragment to retreive the net amount (charged minus credited).
4335 'charged - '. $class->credited_sql;
4340 Returns an SQL fragment to retreive the amount paid against this invoice.
4346 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4347 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4352 Returns an SQL fragment to retreive the amount credited against this invoice.
4358 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4359 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4362 =item search_sql_where HASHREF
4364 Class method which returns an SQL WHERE fragment to search for parameters
4365 specified in HASHREF. Valid parameters are
4371 List reference of start date, end date, as UNIX timestamps.
4381 List reference of charged limits (exclusive).
4385 List reference of charged limits (exclusive).
4389 flag, return open invoices only
4393 flag, return net invoices only
4397 =item newest_percust
4401 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4405 sub search_sql_where {
4406 my($class, $param) = @_;
4408 warn "$me search_sql_where called with params: \n".
4409 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4415 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4416 push @search, "cust_main.agentnum = $1";
4420 if ( $param->{_date} ) {
4421 my($beginning, $ending) = @{$param->{_date}};
4423 push @search, "cust_bill._date >= $beginning",
4424 "cust_bill._date < $ending";
4428 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4429 push @search, "cust_bill.invnum >= $1";
4431 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4432 push @search, "cust_bill.invnum <= $1";
4436 if ( $param->{charged} ) {
4437 my @charged = ref($param->{charged})
4438 ? @{ $param->{charged} }
4439 : ($param->{charged});
4441 push @search, map { s/^charged/cust_bill.charged/; $_; }
4445 my $owed_sql = FS::cust_bill->owed_sql;
4448 if ( $param->{owed} ) {
4449 my @owed = ref($param->{owed})
4450 ? @{ $param->{owed} }
4452 push @search, map { s/^owed/$owed_sql/; $_; }
4457 push @search, "0 != $owed_sql"
4458 if $param->{'open'};
4459 push @search, '0 != '. FS::cust_bill->net_sql
4463 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4464 if $param->{'days'};
4467 if ( $param->{'newest_percust'} ) {
4469 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4470 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4472 my @newest_where = map { my $x = $_;
4473 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4476 grep ! /^cust_main./, @search;
4477 my $newest_where = scalar(@newest_where)
4478 ? ' AND '. join(' AND ', @newest_where)
4482 push @search, "cust_bill._date = (
4483 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4484 WHERE newest_cust_bill.custnum = cust_bill.custnum
4490 #agent virtualization
4491 my $curuser = $FS::CurrentUser::CurrentUser;
4492 if ( $curuser->username eq 'fs_queue'
4493 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4495 my $newuser = qsearchs('access_user', {
4496 'username' => $username,
4500 $curuser = $newuser;
4502 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4505 push @search, $curuser->agentnums_sql;
4507 join(' AND ', @search );
4519 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4520 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base