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 || $conf->exists('previous_balance-summary_only') )
2636 unshift @sections, $previous_section if $pr_total;
2639 foreach my $tax ( $self->_items_tax ) {
2641 $taxtotal += $tax->{'amount'};
2643 my $description = &$escape_function( $tax->{'description'} );
2644 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2646 if ( $multisection ) {
2648 my $money = $old_latex ? '' : $money_char;
2649 push @detail_items, {
2650 ext_description => [],
2653 description => $description,
2654 amount => $money. $amount,
2656 section => $tax_section,
2661 push @total_items, {
2662 'total_item' => $description,
2663 'total_amount' => $other_money_char. $amount,
2668 push @buf,[ $description,
2669 $money_char. $amount,
2676 $total->{'total_item'} = 'Sub-total';
2677 $total->{'total_amount'} =
2678 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2680 if ( $multisection ) {
2681 $tax_section->{'subtotal'} = $other_money_char.
2682 sprintf('%.2f', $taxtotal);
2683 $tax_section->{'pretotal'} = 'New charges sub-total '.
2684 $total->{'total_amount'};
2685 push @sections, $tax_section if $taxtotal;
2687 unshift @total_items, $total;
2690 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2692 push @buf,['','-----------'];
2693 push @buf,[( $conf->exists('disable_previous_balance')
2695 : 'Total New Charges'
2697 $money_char. sprintf("%10.2f",$self->charged) ];
2702 $total->{'total_item'} = &$embolden_function('Total');
2703 $total->{'total_amount'} =
2704 &$embolden_function(
2707 $self->charged + ( $conf->exists('disable_previous_balance')
2713 if ( $multisection ) {
2714 if ( $adjust_section->{'sort_weight'} ) {
2715 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2716 sprintf("%.2f", ($self->billing_balance || 0) );
2718 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2719 sprintf('%.2f', $self->charged );
2722 push @total_items, $total;
2724 push @buf,['','-----------'];
2725 push @buf,['Total Charges',
2727 sprintf( '%10.2f', $self->charged +
2728 ( $conf->exists('disable_previous_balance')
2737 unless ( $conf->exists('disable_previous_balance') ) {
2738 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2741 my $credittotal = 0;
2742 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2745 $total->{'total_item'} = &$escape_function($credit->{'description'});
2746 $credittotal += $credit->{'amount'};
2747 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2748 $adjusttotal += $credit->{'amount'};
2749 if ( $multisection ) {
2750 my $money = $old_latex ? '' : $money_char;
2751 push @detail_items, {
2752 ext_description => [],
2755 description => &$escape_function($credit->{'description'}),
2756 amount => $money. $credit->{'amount'},
2758 section => $adjust_section,
2761 push @total_items, $total;
2765 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2768 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2769 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2773 my $paymenttotal = 0;
2774 foreach my $payment ( $self->_items_payments ) {
2776 $total->{'total_item'} = &$escape_function($payment->{'description'});
2777 $paymenttotal += $payment->{'amount'};
2778 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2779 $adjusttotal += $payment->{'amount'};
2780 if ( $multisection ) {
2781 my $money = $old_latex ? '' : $money_char;
2782 push @detail_items, {
2783 ext_description => [],
2786 description => &$escape_function($payment->{'description'}),
2787 amount => $money. $payment->{'amount'},
2789 section => $adjust_section,
2792 push @total_items, $total;
2794 push @buf, [ $payment->{'description'},
2795 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2798 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2800 if ( $multisection ) {
2801 $adjust_section->{'subtotal'} = $other_money_char.
2802 sprintf('%.2f', $adjusttotal);
2803 push @sections, $adjust_section
2804 unless $adjust_section->{sort_weight};
2809 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2810 $total->{'total_amount'} =
2811 &$embolden_function(
2812 $other_money_char. sprintf('%.2f', $summarypage
2814 $self->billing_balance
2815 : $self->owed + $pr_total
2818 if ( $multisection && !$adjust_section->{sort_weight} ) {
2819 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2820 $total->{'total_amount'};
2822 push @total_items, $total;
2824 push @buf,['','-----------'];
2825 push @buf,[$self->balance_due_msg, $money_char.
2826 sprintf("%10.2f", $balance_due ) ];
2830 if ( $multisection ) {
2831 if ($conf->exists('svc_phone_sections')) {
2833 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2834 $total->{'total_amount'} =
2835 &$embolden_function(
2836 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2838 my $last_section = pop @sections;
2839 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2840 $total->{'total_amount'};
2841 push @sections, $last_section;
2843 push @sections, @$late_sections
2847 my @includelist = ();
2848 push @includelist, 'summary' if $summarypage;
2849 foreach my $include ( @includelist ) {
2851 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2854 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2856 @inc_src = $conf->config($inc_file, $agentnum);
2860 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2862 my $convert_map = $convert_maps{$format}{$include};
2864 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2865 s/--\@\]/$delimiters{$format}[1]/g;
2868 &$convert_map( $conf->config($inc_file, $agentnum) );
2872 my $inc_tt = new Text::Template (
2874 SOURCE => [ map "$_\n", @inc_src ],
2875 DELIMITERS => $delimiters{$format},
2876 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2878 unless ( $inc_tt->compile() ) {
2879 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2880 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2884 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2886 $invoice_data{$include} =~ s/\n+$//
2887 if ($format eq 'latex');
2892 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2893 /invoice_lines\((\d*)\)/;
2894 $invoice_lines += $1 || scalar(@buf);
2897 die "no invoice_lines() functions in template?"
2898 if ( $format eq 'template' && !$wasfunc );
2900 if ($format eq 'template') {
2902 if ( $invoice_lines ) {
2903 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2904 $invoice_data{'total_pages'}++
2905 if scalar(@buf) % $invoice_lines;
2908 #setup subroutine for the template
2909 sub FS::cust_bill::_template::invoice_lines {
2910 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2912 scalar(@FS::cust_bill::_template::buf)
2913 ? shift @FS::cust_bill::_template::buf
2922 push @collect, split("\n",
2923 $text_template->fill_in( HASH => \%invoice_data,
2924 PACKAGE => 'FS::cust_bill::_template'
2927 $FS::cust_bill::_template::page++;
2929 map "$_\n", @collect;
2931 warn "filling in template for invoice ". $self->invnum. "\n"
2933 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2936 $text_template->fill_in(HASH => \%invoice_data);
2940 # helper routine for generating date ranges
2941 sub _prior_month30s {
2944 [ 1, 2592000 ], # 0-30 days ago
2945 [ 2592000, 5184000 ], # 30-60 days ago
2946 [ 5184000, 7776000 ], # 60-90 days ago
2947 [ 7776000, 0 ], # 90+ days ago
2950 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2951 $_->[1] ? $self->_date - $_->[1] - 1 : '',
2956 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2958 Returns an postscript invoice, as a scalar.
2960 Options can be passed as a hashref (recommended) or as a list of time, template
2961 and then any key/value pairs for any other options.
2963 I<time> an optional value used to control the printing of overdue messages. The
2964 default is now. It isn't the date of the invoice; that's the `_date' field.
2965 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2966 L<Time::Local> and L<Date::Parse> for conversion functions.
2968 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2975 my ($file, $lfile) = $self->print_latex(@_);
2976 my $ps = generate_ps($file);
2982 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2984 Returns an PDF invoice, as a scalar.
2986 Options can be passed as a hashref (recommended) or as a list of time, template
2987 and then any key/value pairs for any other options.
2989 I<time> an optional value used to control the printing of overdue messages. The
2990 default is now. It isn't the date of the invoice; that's the `_date' field.
2991 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2992 L<Time::Local> and L<Date::Parse> for conversion functions.
2994 I<template>, if specified, is the name of a suffix for alternate invoices.
2996 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3003 my ($file, $lfile) = $self->print_latex(@_);
3004 my $pdf = generate_pdf($file);
3010 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3012 Returns an HTML invoice, as a scalar.
3014 I<time> an optional value used to control the printing of overdue messages. The
3015 default is now. It isn't the date of the invoice; that's the `_date' field.
3016 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3017 L<Time::Local> and L<Date::Parse> for conversion functions.
3019 I<template>, if specified, is the name of a suffix for alternate invoices.
3021 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3023 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3024 when emailing the invoice as part of a multipart/related MIME email.
3032 %params = %{ shift() };
3034 $params{'time'} = shift;
3035 $params{'template'} = shift;
3036 $params{'cid'} = shift;
3039 $params{'format'} = 'html';
3041 $self->print_generic( %params );
3044 # quick subroutine for print_latex
3046 # There are ten characters that LaTeX treats as special characters, which
3047 # means that they do not simply typeset themselves:
3048 # # $ % & ~ _ ^ \ { }
3050 # TeX ignores blanks following an escaped character; if you want a blank (as
3051 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3055 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3056 $value =~ s/([<>])/\$$1\$/g;
3060 #utility methods for print_*
3062 sub _translate_old_latex_format {
3063 warn "_translate_old_latex_format called\n"
3070 if ( $line =~ /^%%Detail\s*$/ ) {
3072 push @template, q![@--!,
3073 q! foreach my $_tr_line (@detail_items) {!,
3074 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3075 q! $_tr_line->{'description'} .= !,
3076 q! "\\tabularnewline\n~~".!,
3077 q! join( "\\tabularnewline\n~~",!,
3078 q! @{$_tr_line->{'ext_description'}}!,
3082 while ( ( my $line_item_line = shift )
3083 !~ /^%%EndDetail\s*$/ ) {
3084 $line_item_line =~ s/'/\\'/g; # nice LTS
3085 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3086 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3087 push @template, " \$OUT .= '$line_item_line';";
3090 push @template, '}',
3093 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3095 push @template, '[@--',
3096 ' foreach my $_tr_line (@total_items) {';
3098 while ( ( my $total_item_line = shift )
3099 !~ /^%%EndTotalDetails\s*$/ ) {
3100 $total_item_line =~ s/'/\\'/g; # nice LTS
3101 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3102 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3103 push @template, " \$OUT .= '$total_item_line';";
3106 push @template, '}',
3110 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3111 push @template, $line;
3117 warn "$_\n" foreach @template;
3126 #check for an invoice-specific override
3127 return $self->invoice_terms if $self->invoice_terms;
3129 #check for a customer- specific override
3130 my $cust_main = $self->cust_main;
3131 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3133 #use configured default
3134 $conf->config('invoice_default_terms') || '';
3140 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3141 $duedate = $self->_date() + ( $1 * 86400 );
3148 $self->due_date ? time2str(shift, $self->due_date) : '';
3151 sub balance_due_msg {
3153 my $msg = 'Balance Due';
3154 return $msg unless $self->terms;
3155 if ( $self->due_date ) {
3156 $msg .= ' - Please pay by '. $self->due_date2str('%x');
3157 } elsif ( $self->terms ) {
3158 $msg .= ' - '. $self->terms;
3163 sub balance_due_date {
3166 if ( $conf->exists('invoice_default_terms')
3167 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3168 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3173 =item invnum_date_pretty
3175 Returns a string with the invoice number and date, for example:
3176 "Invoice #54 (3/20/2008)"
3180 sub invnum_date_pretty {
3182 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3187 Returns a string with the date, for example: "3/20/2008"
3193 time2str('%x', $self->_date);
3196 use vars qw(%pkg_category_cache);
3197 sub _items_sections {
3200 my $summarypage = shift;
3202 my $extra_sections = shift;
3206 my %late_subtotal = ();
3209 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3212 my $usage = $cust_bill_pkg->usage;
3214 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3215 next if ( $display->summary && $summarypage );
3217 my $section = $display->section;
3218 my $type = $display->type;
3220 $not_tax{$section} = 1
3221 unless $cust_bill_pkg->pkgnum == 0;
3223 if ( $display->post_total && !$summarypage ) {
3224 if (! $type || $type eq 'S') {
3225 $late_subtotal{$section} += $cust_bill_pkg->setup
3226 if $cust_bill_pkg->setup != 0;
3230 $late_subtotal{$section} += $cust_bill_pkg->recur
3231 if $cust_bill_pkg->recur != 0;
3234 if ($type && $type eq 'R') {
3235 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3236 if $cust_bill_pkg->recur != 0;
3239 if ($type && $type eq 'U') {
3240 $late_subtotal{$section} += $usage
3241 unless scalar(@$extra_sections);
3246 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3248 if (! $type || $type eq 'S') {
3249 $subtotal{$section} += $cust_bill_pkg->setup
3250 if $cust_bill_pkg->setup != 0;
3254 $subtotal{$section} += $cust_bill_pkg->recur
3255 if $cust_bill_pkg->recur != 0;
3258 if ($type && $type eq 'R') {
3259 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3260 if $cust_bill_pkg->recur != 0;
3263 if ($type && $type eq 'U') {
3264 $subtotal{$section} += $usage
3265 unless scalar(@$extra_sections);
3274 %pkg_category_cache = ();
3276 push @$late, map { { 'description' => &{$escape}($_),
3277 'subtotal' => $late_subtotal{$_},
3279 'sort_weight' => ( _pkg_category($_)
3280 ? _pkg_category($_)->weight
3283 ((_pkg_category($_) && _pkg_category($_)->condense)
3284 ? $self->_condense_section($format)
3288 sort _sectionsort keys %late_subtotal;
3291 if ( $summarypage ) {
3292 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3293 map { $_->categoryname } qsearch('pkg_category', {});
3295 @sections = keys %subtotal;
3298 my @early = map { { 'description' => &{$escape}($_),
3299 'subtotal' => $subtotal{$_},
3300 'summarized' => $not_tax{$_} ? '' : 'Y',
3301 'tax_section' => $not_tax{$_} ? '' : 'Y',
3302 'sort_weight' => ( _pkg_category($_)
3303 ? _pkg_category($_)->weight
3306 ((_pkg_category($_) && _pkg_category($_)->condense)
3307 ? $self->_condense_section($format)
3312 push @early, @$extra_sections if $extra_sections;
3314 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3318 #helper subs for above
3321 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3325 my $categoryname = shift;
3326 $pkg_category_cache{$categoryname} ||=
3327 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3330 my %condensed_format = (
3331 'label' => [ qw( Description Qty Amount ) ],
3333 sub { shift->{description} },
3334 sub { shift->{quantity} },
3335 sub { shift->{amount} },
3337 'align' => [ qw( l r r ) ],
3338 'span' => [ qw( 5 1 1 ) ], # unitprices?
3339 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3342 sub _condense_section {
3343 my ( $self, $format ) = ( shift, shift );
3345 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3346 qw( description_generator
3349 total_line_generator
3354 sub _condensed_generator_defaults {
3355 my ( $self, $format ) = ( shift, shift );
3356 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3365 sub _condensed_header_generator {
3366 my ( $self, $format ) = ( shift, shift );
3368 my ( $f, $prefix, $suffix, $separator, $column ) =
3369 _condensed_generator_defaults($format);
3371 if ($format eq 'latex') {
3372 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3373 $suffix = "\\\\\n\\hline";
3376 sub { my ($d,$a,$s,$w) = @_;
3377 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3379 } elsif ( $format eq 'html' ) {
3380 $prefix = '<th></th>';
3384 sub { my ($d,$a,$s,$w) = @_;
3385 return qq!<th align="$html_align{$a}">$d</th>!;
3393 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3395 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3398 $prefix. join($separator, @result). $suffix;
3403 sub _condensed_description_generator {
3404 my ( $self, $format ) = ( shift, shift );
3406 my ( $f, $prefix, $suffix, $separator, $column ) =
3407 _condensed_generator_defaults($format);
3409 if ($format eq 'latex') {
3410 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3412 $separator = " & \n";
3414 sub { my ($d,$a,$s,$w) = @_;
3415 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3417 }elsif ( $format eq 'html' ) {
3418 $prefix = '"><td align="center"></td>';
3422 sub { my ($d,$a,$s,$w) = @_;
3423 return qq!<td align="$html_align{$a}">$d</td>!;
3431 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3432 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3433 map { $f->{$_}->[$i] } qw(align span width)
3437 $prefix. join( $separator, @result ). $suffix;
3442 sub _condensed_total_generator {
3443 my ( $self, $format ) = ( shift, shift );
3445 my ( $f, $prefix, $suffix, $separator, $column ) =
3446 _condensed_generator_defaults($format);
3449 if ($format eq 'latex') {
3452 $separator = " & \n";
3454 sub { my ($d,$a,$s,$w) = @_;
3455 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3457 }elsif ( $format eq 'html' ) {
3461 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3463 sub { my ($d,$a,$s,$w) = @_;
3464 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3473 # my $r = &{$f->{fields}->[$i]}(@args);
3474 # $r .= ' Total' unless $i;
3476 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3478 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3479 map { $f->{$_}->[$i] } qw(align span width)
3483 $prefix. join( $separator, @result ). $suffix;
3488 =item total_line_generator FORMAT
3490 Returns a coderef used for generation of invoice total line items for this
3491 usage_class. FORMAT is either html or latex
3495 # should not be used: will have issues with hash element names (description vs
3496 # total_item and amount vs total_amount -- another array of functions?
3498 sub _condensed_total_line_generator {
3499 my ( $self, $format ) = ( shift, shift );
3501 my ( $f, $prefix, $suffix, $separator, $column ) =
3502 _condensed_generator_defaults($format);
3505 if ($format eq 'latex') {
3508 $separator = " & \n";
3510 sub { my ($d,$a,$s,$w) = @_;
3511 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3513 }elsif ( $format eq 'html' ) {
3517 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3519 sub { my ($d,$a,$s,$w) = @_;
3520 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3529 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3531 &{$column}( &{$f->{fields}->[$i]}(@args),
3532 map { $f->{$_}->[$i] } qw(align span width)
3536 $prefix. join( $separator, @result ). $suffix;
3541 #sub _items_extra_usage_sections {
3543 # my $escape = shift;
3545 # my %sections = ();
3547 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3548 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3550 # next unless $cust_bill_pkg->pkgnum > 0;
3552 # foreach my $section ( keys %usage_class ) {
3554 # my $usage = $cust_bill_pkg->usage($section);
3556 # next unless $usage && $usage > 0;
3558 # $sections{$section} ||= 0;
3559 # $sections{$section} += $usage;
3565 # map { { 'description' => &{$escape}($_),
3566 # 'subtotal' => $sections{$_},
3567 # 'summarized' => '',
3568 # 'tax_section' => '',
3571 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3575 sub _items_extra_usage_sections {
3584 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3585 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3586 next unless $cust_bill_pkg->pkgnum > 0;
3588 foreach my $classnum ( keys %usage_class ) {
3589 my $section = $usage_class{$classnum}->classname;
3590 $classnums{$section} = $classnum;
3592 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3593 my $amount = $detail->amount;
3594 next unless $amount && $amount > 0;
3596 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3597 $sections{$section}{amount} += $amount; #subtotal
3598 $sections{$section}{calls}++;
3599 $sections{$section}{duration} += $detail->duration;
3601 my $desc = $detail->regionname;
3602 my $description = $desc;
3603 $description = substr($desc, 0, 50). '...'
3604 if $format eq 'latex' && length($desc) > 50;
3606 $lines{$section}{$desc} ||= {
3607 description => &{$escape}($description),
3608 #pkgpart => $part_pkg->pkgpart,
3609 pkgnum => $cust_bill_pkg->pkgnum,
3614 #unit_amount => $cust_bill_pkg->unitrecur,
3615 quantity => $cust_bill_pkg->quantity,
3616 product_code => 'N/A',
3617 ext_description => [],
3620 $lines{$section}{$desc}{amount} += $amount;
3621 $lines{$section}{$desc}{calls}++;
3622 $lines{$section}{$desc}{duration} += $detail->duration;
3628 my %sectionmap = ();
3629 foreach (keys %sections) {
3630 my $usage_class = $usage_class{$classnums{$_}};
3631 $sectionmap{$_} = { 'description' => &{$escape}($_),
3632 'amount' => $sections{$_}{amount}, #subtotal
3633 'calls' => $sections{$_}{calls},
3634 'duration' => $sections{$_}{duration},
3636 'tax_section' => '',
3637 'sort_weight' => $usage_class->weight,
3638 ( $usage_class->format
3639 ? ( map { $_ => $usage_class->$_($format) }
3640 qw( description_generator header_generator total_generator total_line_generator )
3647 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3651 foreach my $section ( keys %lines ) {
3652 foreach my $line ( keys %{$lines{$section}} ) {
3653 my $l = $lines{$section}{$line};
3654 $l->{section} = $sectionmap{$section};
3655 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3656 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3661 return(\@sections, \@lines);
3665 sub _items_svc_phone_sections {
3674 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3676 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3677 next unless $cust_bill_pkg->pkgnum > 0;
3679 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3681 my $phonenum = $detail->phonenum;
3682 next unless $phonenum;
3684 my $amount = $detail->amount;
3685 next unless $amount && $amount > 0;
3687 $sections{$phonenum} ||= { 'amount' => 0,
3690 'sort_weight' => -1,
3691 'phonenum' => $phonenum,
3693 $sections{$phonenum}{amount} += $amount; #subtotal
3694 $sections{$phonenum}{calls}++;
3695 $sections{$phonenum}{duration} += $detail->duration;
3697 my $desc = $detail->regionname;
3698 my $description = $desc;
3699 $description = substr($desc, 0, 50). '...'
3700 if $format eq 'latex' && length($desc) > 50;
3702 $lines{$phonenum}{$desc} ||= {
3703 description => &{$escape}($description),
3704 #pkgpart => $part_pkg->pkgpart,
3712 product_code => 'N/A',
3713 ext_description => [],
3716 $lines{$phonenum}{$desc}{amount} += $amount;
3717 $lines{$phonenum}{$desc}{calls}++;
3718 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3720 my $line = $usage_class{$detail->classnum}->classname;
3721 $sections{"$phonenum $line"} ||=
3725 'sort_weight' => $usage_class{$detail->classnum}->weight,
3726 'phonenum' => $phonenum,
3728 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3729 $sections{"$phonenum $line"}{calls}++;
3730 $sections{"$phonenum $line"}{duration} += $detail->duration;
3732 $lines{"$phonenum $line"}{$desc} ||= {
3733 description => &{$escape}($description),
3734 #pkgpart => $part_pkg->pkgpart,
3742 product_code => 'N/A',
3743 ext_description => [],
3746 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3747 $lines{"$phonenum $line"}{$desc}{calls}++;
3748 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3749 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3750 $detail->formatted('format' => $format);
3755 my %sectionmap = ();
3756 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3757 my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3758 foreach ( keys %sections ) {
3759 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3760 my $usage_class = $summary ? $simple : $usage_simple;
3761 my $ending = $summary ? ' usage charges' : '';
3762 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3763 'amount' => $sections{$_}{amount}, #subtotal
3764 'calls' => $sections{$_}{calls},
3765 'duration' => $sections{$_}{duration},
3767 'tax_section' => '',
3768 'phonenum' => $sections{$_}{phonenum},
3769 'sort_weight' => $sections{$_}{sort_weight},
3770 'post_total' => $summary, #inspire pagebreak
3772 ( map { $_ => $usage_class->$_($format) }
3773 qw( description_generator
3776 total_line_generator
3783 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3784 $a->{sort_weight} <=> $b->{sort_weight}
3789 foreach my $section ( keys %lines ) {
3790 foreach my $line ( keys %{$lines{$section}} ) {
3791 my $l = $lines{$section}{$line};
3792 $l->{section} = $sectionmap{$section};
3793 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3794 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3799 return(\@sections, \@lines);
3806 #my @display = scalar(@_)
3808 # : qw( _items_previous _items_pkg );
3809 # #: qw( _items_pkg );
3810 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3811 my @display = qw( _items_previous _items_pkg );
3814 foreach my $display ( @display ) {
3815 push @b, $self->$display(@_);
3820 sub _items_previous {
3822 my $cust_main = $self->cust_main;
3823 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3825 foreach ( @pr_cust_bill ) {
3826 my $date = $conf->exists('invoice_show_prior_due_date')
3827 ? 'due '. $_->due_date2str($date_format)
3828 : time2str('%x', $_->_date); # date_format here, too,
3829 # but fix _items_cust_bill_pkg,
3832 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3833 #'pkgpart' => 'N/A',
3835 'amount' => sprintf("%.2f", $_->owed),
3841 # 'description' => 'Previous Balance',
3842 # #'pkgpart' => 'N/A',
3843 # 'pkgnum' => 'N/A',
3844 # 'amount' => sprintf("%10.2f", $pr_total ),
3845 # 'ext_description' => [ map {
3846 # "Invoice ". $_->invnum.
3847 # " (". time2str("%x",$_->_date). ") ".
3848 # sprintf("%10.2f", $_->owed)
3849 # } @pr_cust_bill ],
3857 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3858 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3859 if ($options{section} && $options{section}->{condensed}) {
3861 local $Storable::canonical = 1;
3862 foreach ( @items ) {
3864 delete $item->{ref};
3865 delete $item->{ext_description};
3866 my $key = freeze($item);
3867 $itemshash{$key} ||= 0;
3868 $itemshash{$key} ++; # += $item->{quantity};
3870 @items = sort { $a->{description} cmp $b->{description} }
3871 map { my $i = thaw($_);
3872 $i->{quantity} = $itemshash{$_};
3874 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3883 return 0 unless $a cmp $b;
3884 return -1 if $b eq 'Tax';
3885 return 1 if $a eq 'Tax';
3886 return -1 if $b eq 'Other surcharges';
3887 return 1 if $a eq 'Other surcharges';
3893 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3894 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3897 sub _items_cust_bill_pkg {
3899 my $cust_bill_pkg = shift;
3902 my $format = $opt{format} || '';
3903 my $escape_function = $opt{escape_function} || sub { shift };
3904 my $format_function = $opt{format_function} || '';
3905 my $unsquelched = $opt{unsquelched} || '';
3906 my $section = $opt{section}->{description} if $opt{section};
3907 my $summary_page = $opt{summary_page} || '';
3908 my $multilocation = $opt{multilocation} || '';
3911 my ($s, $r, $u) = ( undef, undef, undef );
3912 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3915 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3916 if ( $_ && !$cust_bill_pkg->hidden ) {
3917 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3918 $_->{amount} =~ s/^\-0\.00$/0.00/;
3919 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3921 unless $_->{amount} == 0;
3926 foreach my $display ( grep { defined($section)
3927 ? $_->section eq $section
3930 grep { !$_->summary || !$summary_page }
3931 $cust_bill_pkg->cust_bill_pkg_display
3935 my $type = $display->type;
3937 my $desc = $cust_bill_pkg->desc;
3938 $desc = substr($desc, 0, 50). '...'
3939 if $format eq 'latex' && length($desc) > 50;
3941 my %details_opt = ( 'format' => $format,
3942 'escape_function' => $escape_function,
3943 'format_function' => $format_function,
3946 if ( $cust_bill_pkg->pkgnum > 0 ) {
3948 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3950 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3952 my $description = $desc;
3953 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3956 unless ( $cust_pkg->part_pkg->hide_svc_detail
3957 || $cust_bill_pkg->hidden )
3959 push @d, map &{$escape_function}($_),
3960 $cust_pkg->h_labels_short($self->_date);
3961 if ( $multilocation ) {
3962 my $loc = $cust_pkg->location_label;
3963 $loc = substr($desc, 0, 50). '...'
3964 if $format eq 'latex' && length($loc) > 50;
3965 push @d, &{$escape_function}($loc);
3968 push @d, $cust_bill_pkg->details(%details_opt)
3969 if $cust_bill_pkg->recur == 0;
3971 if ( $cust_bill_pkg->hidden ) {
3972 $s->{amount} += $cust_bill_pkg->setup;
3973 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3974 push @{ $s->{ext_description} }, @d;
3977 description => $description,
3978 #pkgpart => $part_pkg->pkgpart,
3979 pkgnum => $cust_bill_pkg->pkgnum,
3980 amount => $cust_bill_pkg->setup,
3981 unit_amount => $cust_bill_pkg->unitsetup,
3982 quantity => $cust_bill_pkg->quantity,
3983 ext_description => \@d,
3989 if ( $cust_bill_pkg->recur != 0 &&
3990 ( !$type || $type eq 'R' || $type eq 'U' )
3994 my $is_summary = $display->summary;
3995 my $description = ($is_summary && $type && $type eq 'U')
3996 ? "Usage charges" : $desc;
3998 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3999 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
4000 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
4005 #at least until cust_bill_pkg has "past" ranges in addition to
4006 #the "future" sdate/edate ones... see #3032
4007 my @dates = ( $self->_date );
4008 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4009 push @dates, $prev->sdate if $prev;
4011 unless ( $cust_pkg->part_pkg->hide_svc_detail
4012 || $cust_bill_pkg->itemdesc
4013 || $cust_bill_pkg->hidden
4014 || $is_summary && $type && $type eq 'U' )
4016 push @d, map &{$escape_function}($_),
4017 $cust_pkg->h_labels_short(@dates)
4018 #$cust_bill_pkg->edate,
4019 #$cust_bill_pkg->sdate)
4021 if ( $multilocation ) {
4022 my $loc = $cust_pkg->location_label;
4023 $loc = substr($desc, 0, 50). '...'
4024 if $format eq 'latex' && length($loc) > 50;
4025 push @d, &{$escape_function}($loc);
4029 push @d, $cust_bill_pkg->details(%details_opt)
4030 unless ($is_summary || $type && $type eq 'R');
4034 $amount = $cust_bill_pkg->recur;
4035 }elsif($type eq 'R') {
4036 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4037 }elsif($type eq 'U') {
4038 $amount = $cust_bill_pkg->usage;
4041 if ( !$type || $type eq 'R' ) {
4043 if ( $cust_bill_pkg->hidden ) {
4044 $r->{amount} += $amount;
4045 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4046 push @{ $r->{ext_description} }, @d;
4049 description => $description,
4050 #pkgpart => $part_pkg->pkgpart,
4051 pkgnum => $cust_bill_pkg->pkgnum,
4053 unit_amount => $cust_bill_pkg->unitrecur,
4054 quantity => $cust_bill_pkg->quantity,
4055 ext_description => \@d,
4059 } elsif ( $amount ) { # && $type eq 'U'
4061 if ( $cust_bill_pkg->hidden ) {
4062 $u->{amount} += $amount;
4063 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4064 push @{ $u->{ext_description} }, @d;
4067 description => $description,
4068 #pkgpart => $part_pkg->pkgpart,
4069 pkgnum => $cust_bill_pkg->pkgnum,
4071 unit_amount => $cust_bill_pkg->unitrecur,
4072 quantity => $cust_bill_pkg->quantity,
4073 ext_description => \@d,
4079 } # recurring or usage with recurring charge
4081 } else { #pkgnum tax or one-shot line item (??)
4083 if ( $cust_bill_pkg->setup != 0 ) {
4085 'description' => $desc,
4086 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4089 if ( $cust_bill_pkg->recur != 0 ) {
4091 'description' => "$desc (".
4092 time2str("%x", $cust_bill_pkg->sdate). ' - '.
4093 time2str("%x", $cust_bill_pkg->edate). ')',
4094 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4104 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4106 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4107 $_->{amount} =~ s/^\-0\.00$/0.00/;
4108 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4110 unless $_->{amount} == 0;
4118 sub _items_credits {
4119 my( $self, %opt ) = @_;
4120 my $trim_len = $opt{'trim_len'} || 60;
4124 foreach ( $self->cust_credited ) {
4126 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4128 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4129 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4130 $reason = " ($reason) " if $reason;
4133 #'description' => 'Credit ref\#'. $_->crednum.
4134 # " (". time2str("%x",$_->cust_credit->_date) .")".
4136 'description' => 'Credit applied '.
4137 time2str("%x",$_->cust_credit->_date). $reason,
4138 'amount' => sprintf("%.2f",$_->amount),
4146 sub _items_payments {
4150 #get & print payments
4151 foreach ( $self->cust_bill_pay ) {
4153 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4156 'description' => "Payment received ".
4157 time2str("%x",$_->cust_pay->_date ),
4158 'amount' => sprintf("%.2f", $_->amount )
4166 =item call_details [ OPTION => VALUE ... ]
4168 Returns an array of CSV strings representing the call details for this invoice
4169 The only option available is the boolean prepend_billed_number
4174 my ($self, %opt) = @_;
4176 my $format_function = sub { shift };
4178 if ($opt{prepend_billed_number}) {
4179 $format_function = sub {
4183 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4188 my @details = map { $_->details( 'format_function' => $format_function,
4189 'escape_function' => sub{ return() },
4193 $self->cust_bill_pkg;
4194 my $header = $details[0];
4195 ( $header, grep { $_ ne $header } @details );
4205 =item process_reprint
4209 sub process_reprint {
4210 process_re_X('print', @_);
4213 =item process_reemail
4217 sub process_reemail {
4218 process_re_X('email', @_);
4226 process_re_X('fax', @_);
4234 process_re_X('ftp', @_);
4241 sub process_respool {
4242 process_re_X('spool', @_);
4245 use Storable qw(thaw);
4249 my( $method, $job ) = ( shift, shift );
4250 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4252 my $param = thaw(decode_base64(shift));
4253 warn Dumper($param) if $DEBUG;
4264 my($method, $job, %param ) = @_;
4266 warn "re_X $method for job $job with param:\n".
4267 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4270 #some false laziness w/search/cust_bill.html
4272 my $orderby = 'ORDER BY cust_bill._date';
4274 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4276 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4278 my @cust_bill = qsearch( {
4279 #'select' => "cust_bill.*",
4280 'table' => 'cust_bill',
4281 'addl_from' => $addl_from,
4283 'extra_sql' => $extra_sql,
4284 'order_by' => $orderby,
4288 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4290 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4293 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4294 foreach my $cust_bill ( @cust_bill ) {
4295 $cust_bill->$method();
4297 if ( $job ) { #progressbar foo
4299 if ( time - $min_sec > $last ) {
4300 my $error = $job->update_statustext(
4301 int( 100 * $num / scalar(@cust_bill) )
4303 die $error if $error;
4314 =head1 CLASS METHODS
4320 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4326 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4331 Returns an SQL fragment to retreive the net amount (charged minus credited).
4337 'charged - '. $class->credited_sql;
4342 Returns an SQL fragment to retreive the amount paid against this invoice.
4348 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4349 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4354 Returns an SQL fragment to retreive the amount credited against this invoice.
4360 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4361 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4364 =item search_sql_where HASHREF
4366 Class method which returns an SQL WHERE fragment to search for parameters
4367 specified in HASHREF. Valid parameters are
4373 List reference of start date, end date, as UNIX timestamps.
4383 List reference of charged limits (exclusive).
4387 List reference of charged limits (exclusive).
4391 flag, return open invoices only
4395 flag, return net invoices only
4399 =item newest_percust
4403 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4407 sub search_sql_where {
4408 my($class, $param) = @_;
4410 warn "$me search_sql_where called with params: \n".
4411 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4417 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4418 push @search, "cust_main.agentnum = $1";
4422 if ( $param->{_date} ) {
4423 my($beginning, $ending) = @{$param->{_date}};
4425 push @search, "cust_bill._date >= $beginning",
4426 "cust_bill._date < $ending";
4430 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4431 push @search, "cust_bill.invnum >= $1";
4433 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4434 push @search, "cust_bill.invnum <= $1";
4438 if ( $param->{charged} ) {
4439 my @charged = ref($param->{charged})
4440 ? @{ $param->{charged} }
4441 : ($param->{charged});
4443 push @search, map { s/^charged/cust_bill.charged/; $_; }
4447 my $owed_sql = FS::cust_bill->owed_sql;
4450 if ( $param->{owed} ) {
4451 my @owed = ref($param->{owed})
4452 ? @{ $param->{owed} }
4454 push @search, map { s/^owed/$owed_sql/; $_; }
4459 push @search, "0 != $owed_sql"
4460 if $param->{'open'};
4461 push @search, '0 != '. FS::cust_bill->net_sql
4465 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4466 if $param->{'days'};
4469 if ( $param->{'newest_percust'} ) {
4471 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4472 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4474 my @newest_where = map { my $x = $_;
4475 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4478 grep ! /^cust_main./, @search;
4479 my $newest_where = scalar(@newest_where)
4480 ? ' AND '. join(' AND ', @newest_where)
4484 push @search, "cust_bill._date = (
4485 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4486 WHERE newest_cust_bill.custnum = cust_bill.custnum
4492 #agent virtualization
4493 my $curuser = $FS::CurrentUser::CurrentUser;
4494 if ( $curuser->username eq 'fs_queue'
4495 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4497 my $newuser = qsearchs('access_user', {
4498 'username' => $username,
4502 $curuser = $newuser;
4504 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4507 push @search, $curuser->agentnums_sql;
4509 join(' AND ', @search );
4521 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4522 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base