4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_statement;
20 use FS::cust_bill_pkg;
21 use FS::cust_bill_pkg_display;
25 use FS::cust_credit_bill;
27 use FS::cust_pay_batch;
28 use FS::cust_bill_event;
31 use FS::cust_bill_pay;
32 use FS::cust_bill_pay_batch;
33 use FS::part_bill_event;
36 @ISA = qw( FS::cust_main_Mixin FS::Record );
39 $me = '[FS::cust_bill]';
41 #ask FS::UID to run this stuff for us later
42 FS::UID->install_callback( sub {
44 $money_char = $conf->config('money_char') || '$';
49 FS::cust_bill - Object methods for cust_bill records
55 $record = new FS::cust_bill \%hash;
56 $record = new FS::cust_bill { 'column' => 'value' };
58 $error = $record->insert;
60 $error = $new_record->replace($old_record);
62 $error = $record->delete;
64 $error = $record->check;
66 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
68 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
70 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
72 @cust_pay_objects = $cust_bill->cust_pay;
74 $tax_amount = $record->tax;
76 @lines = $cust_bill->print_text;
77 @lines = $cust_bill->print_text $time;
81 An FS::cust_bill object represents an invoice; a declaration that a customer
82 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
83 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
84 following fields are currently supported:
90 =item invnum - primary key (assigned automatically for new invoices)
92 =item custnum - customer (see L<FS::cust_main>)
94 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
95 L<Time::Local> and L<Date::Parse> for conversion functions.
97 =item charged - amount of this invoice
105 =item printed - deprecated
113 =item closed - books closed flag, empty or `Y'
115 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
117 =item agent_invid - legacy invoice number
127 Creates a new invoice. To add the invoice to the database, see L<"insert">.
128 Invoices are normally created by calling the bill method of a customer object
129 (see L<FS::cust_main>).
133 sub table { 'cust_bill'; }
135 sub cust_linked { $_[0]->cust_main_custnum; }
136 sub cust_unlinked_msg {
138 "WARNING: can't find cust_main.custnum ". $self->custnum.
139 ' (cust_bill.invnum '. $self->invnum. ')';
144 Adds this invoice to the database ("Posts" the invoice). If there is an error,
145 returns the error, otherwise returns false.
149 This method now works but you probably shouldn't use it. Instead, apply a
150 credit against the invoice.
152 Using this method to delete invoices outright is really, really bad. There
153 would be no record you ever posted this invoice, and there are no check to
154 make sure charged = 0 or that there are no associated cust_bill_pkg records.
156 Really, don't use it.
162 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
163 $self->SUPER::delete(@_);
166 =item replace OLD_RECORD
168 Replaces the OLD_RECORD with this one in the database. If there is an error,
169 returns the error, otherwise returns false.
171 Only printed may be changed. printed is normally updated by calling the
172 collect method of a customer object (see L<FS::cust_main>).
176 #replace can be inherited from Record.pm
178 # replace_check is now the preferred way to #implement replace data checks
179 # (so $object->replace() works without an argument)
182 my( $new, $old ) = ( shift, shift );
183 return "Can't change custnum!" unless $old->custnum == $new->custnum;
184 #return "Can't change _date!" unless $old->_date eq $new->_date;
185 return "Can't change _date!" unless $old->_date == $new->_date;
186 return "Can't change charged!" unless $old->charged == $new->charged
187 || $old->charged == 0;
194 Checks all fields to make sure this is a valid invoice. If there is an error,
195 returns the error, otherwise returns false. Called by the insert and replace
204 $self->ut_numbern('invnum')
205 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
206 || $self->ut_numbern('_date')
207 || $self->ut_money('charged')
208 || $self->ut_numbern('printed')
209 || $self->ut_enum('closed', [ '', 'Y' ])
210 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
211 || $self->ut_numbern('agent_invid') #varchar?
213 return $error if $error;
215 $self->_date(time) unless $self->_date;
217 $self->printed(0) if $self->printed eq '';
224 Returns the displayed invoice number for this invoice: agent_invid if
225 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
231 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
232 return $self->agent_invid;
234 return $self->invnum;
240 Returns a list consisting of the total previous balance for this customer,
241 followed by the previous outstanding invoices (as FS::cust_bill objects also).
248 my @cust_bill = sort { $a->_date <=> $b->_date }
249 grep { $_->owed != 0 && $_->_date < $self->_date }
250 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
252 foreach ( @cust_bill ) { $total += $_->owed; }
258 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
265 { 'table' => 'cust_bill_pkg',
266 'hashref' => { 'invnum' => $self->invnum },
267 'order_by' => 'ORDER BY billpkgnum',
272 =item cust_bill_pkg_pkgnum PKGNUM
274 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
279 sub cust_bill_pkg_pkgnum {
280 my( $self, $pkgnum ) = @_;
282 { 'table' => 'cust_bill_pkg',
283 'hashref' => { 'invnum' => $self->invnum,
286 'order_by' => 'ORDER BY billpkgnum',
293 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
300 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
302 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
305 =item open_cust_bill_pkg
307 Returns the open line items for this invoice.
309 Note that cust_bill_pkg with both setup and recur fees are returned as two
310 separate line items, each with only one fee.
314 # modeled after cust_main::open_cust_bill
315 sub open_cust_bill_pkg {
318 # grep { $_->owed > 0 } $self->cust_bill_pkg
320 my %other = ( 'recur' => 'setup',
321 'setup' => 'recur', );
323 foreach my $field ( qw( recur setup )) {
324 push @open, map { $_->set( $other{$field}, 0 ); $_; }
325 grep { $_->owed($field) > 0 }
326 $self->cust_bill_pkg;
332 =item cust_bill_event
334 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
338 sub cust_bill_event {
340 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
343 =item num_cust_bill_event
345 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
349 sub num_cust_bill_event {
352 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
353 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
354 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
355 $sth->fetchrow_arrayref->[0];
360 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
364 #false laziness w/cust_pkg.pm
368 'table' => 'cust_event',
369 'addl_from' => 'JOIN part_event USING ( eventpart )',
370 'hashref' => { 'tablenum' => $self->invnum },
371 'extra_sql' => " AND eventtable = 'cust_bill' ",
377 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
381 #false laziness w/cust_pkg.pm
385 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
386 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
387 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
388 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
389 $sth->fetchrow_arrayref->[0];
394 Returns the customer (see L<FS::cust_main>) for this invoice.
400 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
403 =item cust_suspend_if_balance_over AMOUNT
405 Suspends the customer associated with this invoice if the total amount owed on
406 this invoice and all older invoices is greater than the specified amount.
408 Returns a list: an empty list on success or a list of errors.
412 sub cust_suspend_if_balance_over {
413 my( $self, $amount ) = ( shift, shift );
414 my $cust_main = $self->cust_main;
415 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
418 $cust_main->suspend(@_);
424 Depreciated. See the cust_credited method.
426 #Returns a list consisting of the total previous credited (see
427 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
428 #outstanding credits (FS::cust_credit objects).
434 croak "FS::cust_bill->cust_credit depreciated; see ".
435 "FS::cust_bill->cust_credit_bill";
438 #my @cust_credit = sort { $a->_date <=> $b->_date }
439 # grep { $_->credited != 0 && $_->_date < $self->_date }
440 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
442 #foreach (@cust_credit) { $total += $_->credited; }
443 #$total, @cust_credit;
448 Depreciated. See the cust_bill_pay method.
450 #Returns all payments (see L<FS::cust_pay>) for this invoice.
456 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
458 #sort { $a->_date <=> $b->_date }
459 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
465 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
471 sort { $a->_date <=> $b->_date }
472 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
477 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
483 sort { $a->_date <=> $b->_date }
484 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
488 =item cust_bill_pay_pkgnum PKGNUM
490 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
491 with matching pkgnum.
495 sub cust_bill_pay_pkgnum {
496 my( $self, $pkgnum ) = @_;
497 sort { $a->_date <=> $b->_date }
498 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
504 =item cust_credited_pkgnum PKGNUM
506 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
507 with matching pkgnum.
511 sub cust_credited_pkgnum {
512 my( $self, $pkgnum ) = @_;
513 sort { $a->_date <=> $b->_date }
514 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
522 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
529 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
531 foreach (@taxlines) { $total += $_->setup; }
537 Returns the amount owed (still outstanding) on this invoice, which is charged
538 minus all payment applications (see L<FS::cust_bill_pay>) and credit
539 applications (see L<FS::cust_credit_bill>).
545 my $balance = $self->charged;
546 $balance -= $_->amount foreach ( $self->cust_bill_pay );
547 $balance -= $_->amount foreach ( $self->cust_credited );
548 $balance = sprintf( "%.2f", $balance);
549 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
554 my( $self, $pkgnum ) = @_;
556 #my $balance = $self->charged;
558 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
560 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
561 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
563 $balance = sprintf( "%.2f", $balance);
564 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
568 =item apply_payments_and_credits [ OPTION => VALUE ... ]
570 Applies unapplied payments and credits to this invoice.
572 A hash of optional arguments may be passed. Currently "manual" is supported.
573 If true, a payment receipt is sent instead of a statement when
574 'payment_receipt_email' configuration option is set.
576 If there is an error, returns the error, otherwise returns false.
580 sub apply_payments_and_credits {
581 my( $self, %options ) = @_;
583 local $SIG{HUP} = 'IGNORE';
584 local $SIG{INT} = 'IGNORE';
585 local $SIG{QUIT} = 'IGNORE';
586 local $SIG{TERM} = 'IGNORE';
587 local $SIG{TSTP} = 'IGNORE';
588 local $SIG{PIPE} = 'IGNORE';
590 my $oldAutoCommit = $FS::UID::AutoCommit;
591 local $FS::UID::AutoCommit = 0;
594 $self->select_for_update; #mutex
596 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
597 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
599 if ( $conf->exists('pkg-balances') ) {
600 # limit @payments & @credits to those w/ a pkgnum grepped from $self
601 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
602 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
603 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
606 while ( $self->owed > 0 and ( @payments || @credits ) ) {
609 if ( @payments && @credits ) {
611 #decide which goes first by weight of top (unapplied) line item
613 my @open_lineitems = $self->open_cust_bill_pkg;
616 max( map { $_->part_pkg->pay_weight || 0 }
621 my $max_credit_weight =
622 max( map { $_->part_pkg->credit_weight || 0 }
628 #if both are the same... payments first? it has to be something
629 if ( $max_pay_weight >= $max_credit_weight ) {
635 } elsif ( @payments ) {
637 } elsif ( @credits ) {
640 die "guru meditation #12 and 35";
644 if ( $app eq 'pay' ) {
646 my $payment = shift @payments;
647 $unapp_amount = $payment->unapplied;
648 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
649 $app->pkgnum( $payment->pkgnum )
650 if $conf->exists('pkg-balances') && $payment->pkgnum;
652 } elsif ( $app eq 'credit' ) {
654 my $credit = shift @credits;
655 $unapp_amount = $credit->credited;
656 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
657 $app->pkgnum( $credit->pkgnum )
658 if $conf->exists('pkg-balances') && $credit->pkgnum;
661 die "guru meditation #12 and 35";
665 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
666 warn "owed_pkgnum ". $app->pkgnum;
667 $owed = $self->owed_pkgnum($app->pkgnum);
671 next unless $owed > 0;
673 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
674 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
676 $app->invnum( $self->invnum );
678 my $error = $app->insert(%options);
680 $dbh->rollback if $oldAutoCommit;
681 return "Error inserting ". $app->table. " record: $error";
683 die $error if $error;
687 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
692 =item generate_email OPTION => VALUE ...
700 sender address, required
704 alternate template name, optional
708 text attachment arrayref, optional
712 email subject, optional
716 Returns an argument list to be passed to L<FS::Misc::send_email>.
727 my $me = '[FS::cust_bill::generate_email]';
730 'from' => $args{'from'},
731 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
734 my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
736 if (ref($args{'to'}) eq 'ARRAY') {
737 $return{'to'} = $args{'to'};
739 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
740 $self->cust_main->invoicing_list
744 if ( $conf->exists('invoice_html') ) {
746 warn "$me creating HTML/text multipart message"
749 $return{'nobody'} = 1;
751 my $alternative = build MIME::Entity
752 'Type' => 'multipart/alternative',
753 'Encoding' => '7bit',
754 'Disposition' => 'inline'
758 if ( $conf->exists('invoice_email_pdf')
759 and scalar($conf->config('invoice_email_pdf_note')) ) {
761 warn "$me using 'invoice_email_pdf_note' in multipart message"
763 $data = [ map { $_ . "\n" }
764 $conf->config('invoice_email_pdf_note')
769 warn "$me not using 'invoice_email_pdf_note' in multipart message"
771 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
772 $data = $args{'print_text'};
774 $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
779 $alternative->attach(
780 'Type' => 'text/plain',
781 #'Encoding' => 'quoted-printable',
782 'Encoding' => '7bit',
784 'Disposition' => 'inline',
787 $args{'from'} =~ /\@([\w\.\-]+)/;
788 my $from = $1 || 'example.com';
789 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
792 my $agentnum = $self->cust_main->agentnum;
793 if ( defined($args{'template'}) && length($args{'template'})
794 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
797 $logo = 'logo_'. $args{'template'}. '.png';
801 my $image_data = $conf->config_binary( $logo, $agentnum);
803 my $image = build MIME::Entity
804 'Type' => 'image/png',
805 'Encoding' => 'base64',
806 'Data' => $image_data,
807 'Filename' => 'logo.png',
808 'Content-ID' => "<$content_id>",
811 $alternative->attach(
812 'Type' => 'text/html',
813 'Encoding' => 'quoted-printable',
814 'Data' => [ '<html>',
817 ' '. encode_entities($return{'subject'}),
820 ' <body bgcolor="#e8e8e8">',
821 $self->print_html({ time => '',
822 template => $args{'template'},
829 'Disposition' => 'inline',
830 #'Filename' => 'invoice.pdf',
834 if ( $self->cust_main->email_csv_cdr ) {
836 push @otherparts, build MIME::Entity
837 'Type' => 'text/csv',
838 'Encoding' => '7bit',
839 'Data' => [ map { "$_\n" }
840 $self->call_details('prepend_billed_number' => 1)
842 'Disposition' => 'attachment',
843 'Filename' => 'usage-'. $self->invnum. '.csv',
848 if ( $conf->exists('invoice_email_pdf') ) {
853 # multipart/alternative
859 my $related = build MIME::Entity 'Type' => 'multipart/related',
860 'Encoding' => '7bit';
862 #false laziness w/Misc::send_email
863 $related->head->replace('Content-type',
865 '; boundary="'. $related->head->multipart_boundary. '"'.
866 '; type=multipart/alternative'
869 $related->add_part($alternative);
871 $related->add_part($image);
873 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
875 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
879 #no other attachment:
881 # multipart/alternative
886 $return{'content-type'} = 'multipart/related';
887 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
888 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
889 #$return{'disposition'} = 'inline';
895 if ( $conf->exists('invoice_email_pdf') ) {
896 warn "$me creating PDF attachment"
899 #mime parts arguments a la MIME::Entity->build().
900 $return{'mimeparts'} = [
901 { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
905 if ( $conf->exists('invoice_email_pdf')
906 and scalar($conf->config('invoice_email_pdf_note')) ) {
908 warn "$me using 'invoice_email_pdf_note'"
910 $return{'body'} = [ map { $_ . "\n" }
911 $conf->config('invoice_email_pdf_note')
916 warn "$me not using 'invoice_email_pdf_note'"
918 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
919 $return{'body'} = $args{'print_text'};
921 $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
934 Returns a list suitable for passing to MIME::Entity->build(), representing
935 this invoice as PDF attachment.
942 'Type' => 'application/pdf',
943 'Encoding' => 'base64',
944 'Data' => [ $self->print_pdf(@_) ],
945 'Disposition' => 'attachment',
946 'Filename' => 'invoice-'. $self->invnum. '.pdf',
950 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
952 Sends this invoice to the destinations configured for this customer: sends
953 email, prints and/or faxes. See L<FS::cust_main_invoice>.
955 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
957 AGENTNUM, if specified, means that this invoice will only be sent for customers
958 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
959 single agent) or an arrayref of agentnums.
961 INVOICE_FROM, if specified, overrides the default email invoice From: address.
963 AMOUNT, if specified, only sends the invoice if the total amount owed on this
964 invoice and all older invoices is greater than the specified amount.
971 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
972 or die "invalid invoice number: " . $opt{invnum};
974 my @args = ( $opt{template}, $opt{agentnum} );
975 push @args, $opt{invoice_from}
976 if exists($opt{invoice_from}) && $opt{invoice_from};
978 my $error = $self->send( @args );
979 die $error if $error;
985 my $template = scalar(@_) ? shift : '';
986 if ( scalar(@_) && $_[0] ) {
987 my $agentnums = ref($_[0]) ? shift : [ shift ];
988 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
994 : ( $self->_agent_invoice_from || #XXX should go away
995 $conf->config('invoice_from', $self->cust_main->agentnum )
998 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
1001 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1003 my @invoicing_list = $self->cust_main->invoicing_list;
1005 #$self->email_invoice($template, $invoice_from)
1006 $self->email($template, $invoice_from)
1007 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1009 #$self->print_invoice($template)
1010 $self->print($template)
1011 if grep { $_ eq 'POST' } @invoicing_list; #postal
1013 $self->fax_invoice($template)
1014 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1020 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
1022 Emails this invoice.
1024 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1026 INVOICE_FROM, if specified, overrides the default email invoice From: address.
1030 sub queueable_email {
1033 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1034 or die "invalid invoice number: " . $opt{invnum};
1036 my @args = ( $opt{template} );
1037 push @args, $opt{invoice_from}
1038 if exists($opt{invoice_from}) && $opt{invoice_from};
1040 my $error = $self->email( @args );
1041 die $error if $error;
1045 #sub email_invoice {
1048 my $template = scalar(@_) ? shift : '';
1052 : ( $self->_agent_invoice_from || #XXX should go away
1053 $conf->config('invoice_from', $self->cust_main->agentnum )
1057 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1058 $self->cust_main->invoicing_list;
1060 #better to notify this person than silence
1061 @invoicing_list = ($invoice_from) unless @invoicing_list;
1063 my $subject = $self->email_subject($template);
1065 my $error = send_email(
1066 $self->generate_email(
1067 'from' => $invoice_from,
1068 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1069 'subject' => $subject,
1070 'template' => $template,
1073 die "can't email invoice: $error\n" if $error;
1074 #die "$error\n" if $error;
1081 #my $template = scalar(@_) ? shift : '';
1084 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1087 my $cust_main = $self->cust_main;
1088 my $name = $cust_main->name;
1089 my $name_short = $cust_main->name_short;
1090 my $invoice_number = $self->invnum;
1091 my $invoice_date = $self->_date_pretty;
1093 eval qq("$subject");
1096 =item lpr_data [ TEMPLATENAME ]
1098 Returns the postscript or plaintext for this invoice as an arrayref.
1100 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1105 my( $self, $template) = @_;
1106 $conf->exists('invoice_latex')
1107 ? [ $self->print_ps('', $template) ]
1108 : [ $self->print_text('', $template) ];
1111 =item print [ TEMPLATENAME ]
1113 Prints this invoice.
1115 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1119 #sub print_invoice {
1122 my $template = scalar(@_) ? shift : '';
1124 do_print $self->lpr_data($template);
1127 =item fax_invoice [ TEMPLATENAME ]
1131 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1137 my $template = scalar(@_) ? shift : '';
1139 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1140 unless $conf->exists('invoice_latex');
1142 my $dialstring = $self->cust_main->getfield('fax');
1145 my $error = send_fax( 'docdata' => $self->lpr_data($template),
1146 'dialstring' => $dialstring,
1148 die $error if $error;
1152 =item ftp_invoice [ TEMPLATENAME ]
1154 Sends this invoice data via FTP.
1156 TEMPLATENAME is unused?
1162 my $template = scalar(@_) ? shift : '';
1165 'protocol' => 'ftp',
1166 'server' => $conf->config('cust_bill-ftpserver'),
1167 'username' => $conf->config('cust_bill-ftpusername'),
1168 'password' => $conf->config('cust_bill-ftppassword'),
1169 'dir' => $conf->config('cust_bill-ftpdir'),
1170 'format' => $conf->config('cust_bill-ftpformat'),
1174 =item spool_invoice [ TEMPLATENAME ]
1176 Spools this invoice data (see L<FS::spool_csv>)
1178 TEMPLATENAME is unused?
1184 my $template = scalar(@_) ? shift : '';
1187 'format' => $conf->config('cust_bill-spoolformat'),
1188 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1192 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1194 Like B<send>, but only sends the invoice if it is the newest open invoice for
1199 sub send_if_newest {
1204 grep { $_->owed > 0 }
1205 qsearch('cust_bill', {
1206 'custnum' => $self->custnum,
1207 #'_date' => { op=>'>', value=>$self->_date },
1208 'invnum' => { op=>'>', value=>$self->invnum },
1215 =item send_csv OPTION => VALUE, ...
1217 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1221 protocol - currently only "ftp"
1227 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1228 and YYMMDDHHMMSS is a timestamp.
1230 See L</print_csv> for a description of the output format.
1235 my($self, %opt) = @_;
1239 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1240 mkdir $spooldir, 0700 unless -d $spooldir;
1242 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1243 my $file = "$spooldir/$tracctnum.csv";
1245 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1247 open(CSV, ">$file") or die "can't open $file: $!";
1255 if ( $opt{protocol} eq 'ftp' ) {
1256 eval "use Net::FTP;";
1258 $net = Net::FTP->new($opt{server}) or die @$;
1260 die "unknown protocol: $opt{protocol}";
1263 $net->login( $opt{username}, $opt{password} )
1264 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1266 $net->binary or die "can't set binary mode";
1268 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1270 $net->put($file) or die "can't put $file: $!";
1280 Spools CSV invoice data.
1286 =item format - 'default' or 'billco'
1288 =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>).
1290 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1292 =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.
1299 my($self, %opt) = @_;
1301 my $cust_main = $self->cust_main;
1303 if ( $opt{'dest'} ) {
1304 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1305 $cust_main->invoicing_list;
1306 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1307 || ! keys %invoicing_list;
1310 if ( $opt{'balanceover'} ) {
1312 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1315 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1316 mkdir $spooldir, 0700 unless -d $spooldir;
1318 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1322 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1323 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1326 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1328 open(CSV, ">>$file") or die "can't open $file: $!";
1329 flock(CSV, LOCK_EX);
1334 if ( lc($opt{'format'}) eq 'billco' ) {
1336 flock(CSV, LOCK_UN);
1341 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1344 open(CSV,">>$file") or die "can't open $file: $!";
1345 flock(CSV, LOCK_EX);
1351 flock(CSV, LOCK_UN);
1358 =item print_csv OPTION => VALUE, ...
1360 Returns CSV data for this invoice.
1364 format - 'default' or 'billco'
1366 Returns a list consisting of two scalars. The first is a single line of CSV
1367 header information for this invoice. The second is one or more lines of CSV
1368 detail information for this invoice.
1370 If I<format> is not specified or "default", the fields of the CSV file are as
1373 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1377 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1379 B<record_type> is C<cust_bill> for the initial header line only. The
1380 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1381 fields are filled in.
1383 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1384 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1387 =item invnum - invoice number
1389 =item custnum - customer number
1391 =item _date - invoice date
1393 =item charged - total invoice amount
1395 =item first - customer first name
1397 =item last - customer first name
1399 =item company - company name
1401 =item address1 - address line 1
1403 =item address2 - address line 1
1413 =item pkg - line item description
1415 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1417 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1419 =item sdate - start date for recurring fee
1421 =item edate - end date for recurring fee
1425 If I<format> is "billco", the fields of the header CSV file are as follows:
1427 +-------------------------------------------------------------------+
1428 | FORMAT HEADER FILE |
1429 |-------------------------------------------------------------------|
1430 | Field | Description | Name | Type | Width |
1431 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1432 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1433 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1434 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1435 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1436 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1437 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1438 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1439 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1440 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1441 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1442 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1443 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1444 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1445 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1446 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1447 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1448 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1449 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1450 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1451 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1452 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1453 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1454 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1455 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1456 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1457 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1458 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1459 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1460 +-------+-------------------------------+------------+------+-------+
1462 If I<format> is "billco", the fields of the detail CSV file are as follows:
1464 FORMAT FOR DETAIL FILE
1466 Field | Description | Name | Type | Width
1467 1 | N/A-Leave Empty | RC | CHAR | 2
1468 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1469 3 | Account Number | TRACCTNUM | CHAR | 15
1470 4 | Invoice Number | TRINVOICE | CHAR | 15
1471 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1472 6 | Transaction Detail | DETAILS | CHAR | 100
1473 7 | Amount | AMT | NUM* | 9
1474 8 | Line Format Control** | LNCTRL | CHAR | 2
1475 9 | Grouping Code | GROUP | CHAR | 2
1476 10 | User Defined | ACCT CODE | CHAR | 15
1481 my($self, %opt) = @_;
1483 eval "use Text::CSV_XS";
1486 my $cust_main = $self->cust_main;
1488 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1490 if ( lc($opt{'format'}) eq 'billco' ) {
1493 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1495 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1497 my( $previous_balance, @unused ) = $self->previous; #previous balance
1499 my $pmt_cr_applied = 0;
1500 $pmt_cr_applied += $_->{'amount'}
1501 foreach ( $self->_items_payments, $self->_items_credits ) ;
1503 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1506 '', # 1 | N/A-Leave Empty CHAR 2
1507 '', # 2 | N/A-Leave Empty CHAR 15
1508 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1509 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1510 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1511 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1512 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1513 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1514 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1515 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1516 '', # 10 | Ancillary Billing Information CHAR 30
1517 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1518 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1521 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1524 $duedate, # 14 | Bill Due Date CHAR 10
1526 $previous_balance, # 15 | Previous Balance NUM* 9
1527 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1528 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1529 $totaldue, # 18 | Total Amt Due NUM* 9
1530 $totaldue, # 19 | Total Amt Due NUM* 9
1531 '', # 20 | 30 Day Aging NUM* 9
1532 '', # 21 | 60 Day Aging NUM* 9
1533 '', # 22 | 90 Day Aging NUM* 9
1534 'N', # 23 | Y/N CHAR 1
1535 '', # 24 | Remittance automation CHAR 100
1536 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1537 $self->custnum, # 26 | Customer Reference Number CHAR 15
1538 '0', # 27 | Federal Tax*** NUM* 9
1539 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1540 '0', # 29 | Other Taxes & Fees*** NUM* 9
1549 time2str("%x", $self->_date),
1550 sprintf("%.2f", $self->charged),
1551 ( map { $cust_main->getfield($_) }
1552 qw( first last company address1 address2 city state zip country ) ),
1554 ) or die "can't create csv";
1557 my $header = $csv->string. "\n";
1560 if ( lc($opt{'format'}) eq 'billco' ) {
1563 foreach my $item ( $self->_items_pkg ) {
1566 '', # 1 | N/A-Leave Empty CHAR 2
1567 '', # 2 | N/A-Leave Empty CHAR 15
1568 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1569 $self->invnum, # 4 | Invoice Number CHAR 15
1570 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1571 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1572 $item->{'amount'}, # 7 | Amount NUM* 9
1573 '', # 8 | Line Format Control** CHAR 2
1574 '', # 9 | Grouping Code CHAR 2
1575 '', # 10 | User Defined CHAR 15
1578 $detail .= $csv->string. "\n";
1584 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1586 my($pkg, $setup, $recur, $sdate, $edate);
1587 if ( $cust_bill_pkg->pkgnum ) {
1589 ($pkg, $setup, $recur, $sdate, $edate) = (
1590 $cust_bill_pkg->part_pkg->pkg,
1591 ( $cust_bill_pkg->setup != 0
1592 ? sprintf("%.2f", $cust_bill_pkg->setup )
1594 ( $cust_bill_pkg->recur != 0
1595 ? sprintf("%.2f", $cust_bill_pkg->recur )
1597 ( $cust_bill_pkg->sdate
1598 ? time2str("%x", $cust_bill_pkg->sdate)
1600 ($cust_bill_pkg->edate
1601 ?time2str("%x", $cust_bill_pkg->edate)
1605 } else { #pkgnum tax
1606 next unless $cust_bill_pkg->setup != 0;
1607 $pkg = $cust_bill_pkg->desc;
1608 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1609 ( $sdate, $edate ) = ( '', '' );
1615 ( map { '' } (1..11) ),
1616 ($pkg, $setup, $recur, $sdate, $edate)
1617 ) or die "can't create csv";
1619 $detail .= $csv->string. "\n";
1625 ( $header, $detail );
1631 Pays this invoice with a compliemntary payment. If there is an error,
1632 returns the error, otherwise returns false.
1638 my $cust_pay = new FS::cust_pay ( {
1639 'invnum' => $self->invnum,
1640 'paid' => $self->owed,
1643 'payinfo' => $self->cust_main->payinfo,
1651 Attempts to pay this invoice with a credit card payment via a
1652 Business::OnlinePayment realtime gateway. See
1653 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1654 for supported processors.
1660 $self->realtime_bop( 'CC', @_ );
1665 Attempts to pay this invoice with an electronic check (ACH) payment via a
1666 Business::OnlinePayment realtime gateway. See
1667 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1668 for supported processors.
1674 $self->realtime_bop( 'ECHECK', @_ );
1679 Attempts to pay this invoice with phone bill (LEC) payment via a
1680 Business::OnlinePayment realtime gateway. See
1681 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1682 for supported processors.
1688 $self->realtime_bop( 'LEC', @_ );
1692 my( $self, $method ) = @_;
1694 my $cust_main = $self->cust_main;
1695 my $balance = $cust_main->balance;
1696 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1697 $amount = sprintf("%.2f", $amount);
1698 return "not run (balance $balance)" unless $amount > 0;
1700 my $description = 'Internet Services';
1701 if ( $conf->exists('business-onlinepayment-description') ) {
1702 my $dtempl = $conf->config('business-onlinepayment-description');
1704 my $agent_obj = $cust_main->agent
1705 or die "can't retreive agent for $cust_main (agentnum ".
1706 $cust_main->agentnum. ")";
1707 my $agent = $agent_obj->agent;
1708 my $pkgs = join(', ',
1709 map { $_->part_pkg->pkg }
1710 grep { $_->pkgnum } $self->cust_bill_pkg
1712 $description = eval qq("$dtempl");
1715 $cust_main->realtime_bop($method, $amount,
1716 'description' => $description,
1717 'invnum' => $self->invnum,
1722 =item batch_card OPTION => VALUE...
1724 Adds a payment for this invoice to the pending credit card batch (see
1725 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1726 runs the payment using a realtime gateway.
1731 my ($self, %options) = @_;
1732 my $cust_main = $self->cust_main;
1734 $options{invnum} = $self->invnum;
1736 $cust_main->batch_card(%options);
1739 sub _agent_template {
1741 $self->cust_main->agent_template;
1744 sub _agent_invoice_from {
1746 $self->cust_main->agent_invoice_from;
1749 =item print_text [ TIME [ , TEMPLATE ] ]
1751 Returns an text invoice, as a list of lines.
1753 TIME an optional value used to control the printing of overdue messages. The
1754 default is now. It isn't the date of the invoice; that's the `_date' field.
1755 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1756 L<Time::Local> and L<Date::Parse> for conversion functions.
1761 my( $self, $today, $template, %opt ) = @_;
1763 my %params = ( 'format' => 'template' );
1764 $params{'time'} = $today if $today;
1765 $params{'template'} = $template if $template;
1766 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1768 $self->print_generic( %params );
1771 =item print_latex [ TIME [ , TEMPLATE ] ]
1773 Internal method - returns a filename of a filled-in LaTeX template for this
1774 invoice (Note: add ".tex" to get the actual filename), and a filename of
1775 an associated logo (with the .eps extension included).
1777 See print_ps and print_pdf for methods that return PostScript and PDF output.
1779 TIME an optional value used to control the printing of overdue messages. The
1780 default is now. It isn't the date of the invoice; that's the `_date' field.
1781 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1782 L<Time::Local> and L<Date::Parse> for conversion functions.
1787 my( $self, $today, $template, %opt ) = @_;
1789 my %params = ( 'format' => 'latex' );
1790 $params{'time'} = $today if $today;
1791 $params{'template'} = $template if $template;
1792 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1794 $template ||= $self->_agent_template;
1796 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1797 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1801 ) or die "can't open temp file: $!\n";
1803 my $agentnum = $self->cust_main->agentnum;
1805 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1806 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1807 or die "can't write temp file: $!\n";
1809 print $lh $conf->config_binary('logo.eps', $agentnum)
1810 or die "can't write temp file: $!\n";
1813 $params{'logo_file'} = $lh->filename;
1815 my @filled_in = $self->print_generic( %params );
1817 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1821 ) or die "can't open temp file: $!\n";
1822 print $fh join('', @filled_in );
1825 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1826 return ($1, $params{'logo_file'});
1830 =item print_generic OPTIONS_HASH
1832 Internal method - returns a filled-in template for this invoice as a scalar.
1834 See print_ps and print_pdf for methods that return PostScript and PDF output.
1836 Non optional options include
1837 format - latex, html, template
1839 Optional options include
1841 template - a value used as a suffix for a configuration template
1843 time - a value used to control the printing of overdue messages. The
1844 default is now. It isn't the date of the invoice; that's the `_date' field.
1845 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1846 L<Time::Local> and L<Date::Parse> for conversion functions.
1850 unsquelch_cdr - overrides any per customer cdr squelching when true
1854 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1855 # (alignment?) problems to change them all to '%.2f' ?
1858 my( $self, %params ) = @_;
1859 my $today = $params{today} ? $params{today} : time;
1860 warn "$me print_generic called on $self with suffix $params{template}\n"
1863 my $format = $params{format};
1864 die "Unknown format: $format"
1865 unless $format =~ /^(latex|html|template)$/;
1867 my $cust_main = $self->cust_main;
1868 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1869 unless $cust_main->payname
1870 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1872 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1873 'html' => [ '<%=', '%>' ],
1874 'template' => [ '{', '}' ],
1877 #create the template
1878 my $template = $params{template} ? $params{template} : $self->_agent_template;
1879 my $templatefile = "invoice_$format";
1880 $templatefile .= "_$template"
1881 if length($template);
1882 my @invoice_template = map "$_\n", $conf->config($templatefile)
1883 or die "cannot load config data $templatefile";
1886 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1887 #change this to a die when the old code is removed
1888 warn "old-style invoice template $templatefile; ".
1889 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1890 $old_latex = 'true';
1891 @invoice_template = _translate_old_latex_format(@invoice_template);
1894 my $text_template = new Text::Template(
1896 SOURCE => \@invoice_template,
1897 DELIMITERS => $delimiters{$format},
1900 $text_template->compile()
1901 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1904 # additional substitution could possibly cause breakage in existing templates
1905 my %convert_maps = (
1907 'notes' => sub { map "$_", @_ },
1908 'footer' => sub { map "$_", @_ },
1909 'smallfooter' => sub { map "$_", @_ },
1910 'returnaddress' => sub { map "$_", @_ },
1911 'coupon' => sub { map "$_", @_ },
1917 s/%%(.*)$/<!-- $1 -->/g;
1918 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1919 s/\\begin\{enumerate\}/<ol>/g;
1921 s/\\end\{enumerate\}/<\/ol>/g;
1922 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1931 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1933 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1938 s/\\\\\*?\s*$/<BR>/;
1939 s/\\hyphenation\{[\w\s\-]+}//;
1944 'coupon' => sub { "" },
1951 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1952 s/\\begin\{enumerate\}//g;
1954 s/\\end\{enumerate\}//g;
1955 s/\\textbf\{(.*)\}/$1/g;
1962 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1964 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1969 s/\\\\\*?\s*$/\n/; # dubious
1970 s/\\hyphenation\{[\w\s\-]+}//;
1974 'coupon' => sub { "" },
1979 # hashes for differing output formats
1980 my %nbsps = ( 'latex' => '~',
1981 'html' => '', # '&nbps;' would be nice
1982 'template' => '', # not used
1984 my $nbsp = $nbsps{$format};
1986 my %escape_functions = ( 'latex' => \&_latex_escape,
1987 'html' => \&encode_entities,
1988 'template' => sub { shift },
1990 my $escape_function = $escape_functions{$format};
1992 my %date_formats = ( 'latex' => '%b %o, %Y',
1993 'html' => '%b %o, %Y',
1996 my $date_format = $date_formats{$format};
1998 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2000 'html' => sub { return '<b>'. shift(). '</b>'
2002 'template' => sub { shift },
2004 my $embolden_function = $embolden_functions{$format};
2007 # generate template variables
2010 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2014 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2020 $returnaddress = join("\n",
2021 $conf->config_orbase("invoice_${format}returnaddress", $template)
2024 } elsif ( grep /\S/,
2025 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2027 my $convert_map = $convert_maps{$format}{'returnaddress'};
2030 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2035 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2037 my $convert_map = $convert_maps{$format}{'returnaddress'};
2038 $returnaddress = join( "\n", &$convert_map(
2039 map { s/( {2,})/'~' x length($1)/eg;
2043 ( $conf->config('company_name', $self->cust_main->agentnum),
2044 $conf->config('company_address', $self->cust_main->agentnum),
2051 my $warning = "Couldn't find a return address; ".
2052 "do you need to set the company_address configuration value?";
2054 $returnaddress = $nbsp;
2055 #$returnaddress = $warning;
2059 my %invoice_data = (
2060 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2061 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2062 'custnum' => $cust_main->display_custnum,
2063 'invnum' => $self->invnum,
2064 'date' => time2str($date_format, $self->_date),
2065 'today' => time2str('%b %o, %Y', $today),
2066 'agent' => &$escape_function($cust_main->agent->agent),
2067 'agent_custid' => &$escape_function($cust_main->agent_custid),
2068 'payname' => &$escape_function($cust_main->payname),
2069 'company' => &$escape_function($cust_main->company),
2070 'address1' => &$escape_function($cust_main->address1),
2071 'address2' => &$escape_function($cust_main->address2),
2072 'city' => &$escape_function($cust_main->city),
2073 'state' => &$escape_function($cust_main->state),
2074 'zip' => &$escape_function($cust_main->zip),
2075 'fax' => &$escape_function($cust_main->fax),
2076 'returnaddress' => $returnaddress,
2078 'terms' => $self->terms,
2079 'template' => $template, #params{'template'},
2080 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
2081 # better hang on to conf_dir for a while
2082 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2085 'current_charges' => sprintf("%.2f", $self->charged),
2086 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2087 'ship_enable' => $conf->exists('invoice-ship_address'),
2088 'unitprices' => $conf->exists('invoice-unitprice'),
2091 my $countrydefault = $conf->config('countrydefault') || 'US';
2092 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2093 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2094 my $method = $prefix.$_;
2095 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2097 $invoice_data{'ship_country'} = ''
2098 if ( $invoice_data{'ship_country'} eq $countrydefault );
2100 $invoice_data{'cid'} = $params{'cid'}
2103 if ( $cust_main->country eq $countrydefault ) {
2104 $invoice_data{'country'} = '';
2106 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2110 $invoice_data{'address'} = \@address;
2112 $cust_main->payname.
2113 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2114 ? " (P.O. #". $cust_main->payinfo. ")"
2118 push @address, $cust_main->company
2119 if $cust_main->company;
2120 push @address, $cust_main->address1;
2121 push @address, $cust_main->address2
2122 if $cust_main->address2;
2124 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2125 push @address, $invoice_data{'country'}
2126 if $invoice_data{'country'};
2128 while (scalar(@address) < 5);
2130 $invoice_data{'logo_file'} = $params{'logo_file'}
2131 if $params{'logo_file'};
2133 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2134 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2135 #my $balance_due = $self->owed + $pr_total - $cr_total;
2136 my $balance_due = $self->owed + $pr_total;
2137 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2138 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2140 my $agentnum = $self->cust_main->agentnum;
2142 #do variable substitution in notes, footer, smallfooter
2143 foreach my $include (qw( notes footer smallfooter coupon )) {
2145 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2148 if ( $conf->exists($inc_file, $agentnum)
2149 && length( $conf->config($inc_file, $agentnum) ) ) {
2151 @inc_src = $conf->config($inc_file, $agentnum);
2155 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2157 my $convert_map = $convert_maps{$format}{$include};
2159 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2160 s/--\@\]/$delimiters{$format}[1]/g;
2163 &$convert_map( $conf->config($inc_file, $agentnum) );
2167 my $inc_tt = new Text::Template (
2169 SOURCE => [ map "$_\n", @inc_src ],
2170 DELIMITERS => $delimiters{$format},
2171 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2173 unless ( $inc_tt->compile() ) {
2174 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2175 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2179 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2181 $invoice_data{$include} =~ s/\n+$//
2182 if ($format eq 'latex');
2185 $invoice_data{'po_line'} =
2186 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2187 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2190 my %money_chars = ( 'latex' => '',
2191 'html' => $conf->config('money_char') || '$',
2194 my $money_char = $money_chars{$format};
2196 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2197 'html' => $conf->config('money_char') || '$',
2200 my $other_money_char = $other_money_chars{$format};
2202 my @detail_items = ();
2203 my @total_items = ();
2207 $invoice_data{'detail_items'} = \@detail_items;
2208 $invoice_data{'total_items'} = \@total_items;
2209 $invoice_data{'buf'} = \@buf;
2210 $invoice_data{'sections'} = \@sections;
2212 my $previous_section = { 'description' => 'Previous Charges',
2213 'subtotal' => $other_money_char.
2214 sprintf('%.2f', $pr_total),
2218 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2219 'subtotal' => $taxtotal }; # adjusted below
2221 my $adjusttotal = 0;
2222 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2223 'subtotal' => 0 }; # adjusted below
2225 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2226 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2227 my $late_sections = [];
2228 if ( $multisection ) {
2229 push @sections, $self->_items_sections( $late_sections );
2231 push @sections, { 'description' => '', 'subtotal' => '' };
2234 unless ( $conf->exists('disable_previous_balance')
2235 || $conf->exists('previous_balance-summary_only')
2239 foreach my $line_item ( $self->_items_previous ) {
2242 ext_description => [],
2244 $detail->{'ref'} = $line_item->{'pkgnum'};
2245 $detail->{'quantity'} = 1;
2246 $detail->{'section'} = $previous_section;
2247 $detail->{'description'} = &$escape_function($line_item->{'description'});
2248 if ( exists $line_item->{'ext_description'} ) {
2249 @{$detail->{'ext_description'}} = map {
2250 &$escape_function($_);
2251 } @{$line_item->{'ext_description'}};
2253 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2254 $line_item->{'amount'};
2255 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2257 push @detail_items, $detail;
2258 push @buf, [ $detail->{'description'},
2259 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2265 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2266 push @buf, ['','-----------'];
2267 push @buf, [ 'Total Previous Balance',
2268 $money_char. sprintf("%10.2f", $pr_total) ];
2272 foreach my $section (@sections, @$late_sections) {
2274 $section->{'subtotal'} = $other_money_char.
2275 sprintf('%.2f', $section->{'subtotal'})
2278 if ( $section->{'description'} ) {
2279 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2285 $options{'section'} = $section if $multisection;
2286 $options{'format'} = $format;
2287 $options{'escape_function'} = $escape_function;
2288 $options{'format_function'} = sub { () } unless $unsquelched;
2289 $options{'unsquelched'} = $unsquelched;
2291 foreach my $line_item ( $self->_items_pkg(%options) ) {
2293 ext_description => [],
2295 $detail->{'ref'} = $line_item->{'pkgnum'};
2296 $detail->{'quantity'} = $line_item->{'quantity'};
2297 $detail->{'section'} = $section;
2298 $detail->{'description'} = &$escape_function($line_item->{'description'});
2299 if ( exists $line_item->{'ext_description'} ) {
2300 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2302 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2303 $line_item->{'amount'};
2304 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2305 $line_item->{'unit_amount'};
2306 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2308 push @detail_items, $detail;
2309 push @buf, ( [ $detail->{'description'},
2310 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2312 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2316 if ( $section->{'description'} ) {
2317 push @buf, ( ['','-----------'],
2318 [ $section->{'description'}. ' sub-total',
2319 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2328 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2329 unshift @sections, $previous_section if $pr_total;
2332 foreach my $tax ( $self->_items_tax ) {
2334 $taxtotal += $tax->{'amount'};
2336 my $description = &$escape_function( $tax->{'description'} );
2337 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2339 if ( $multisection ) {
2341 my $money = $old_latex ? '' : $money_char;
2342 push @detail_items, {
2343 ext_description => [],
2346 description => $description,
2347 amount => $money. $amount,
2349 section => $tax_section,
2354 push @total_items, {
2355 'total_item' => $description,
2356 'total_amount' => $other_money_char. $amount,
2361 push @buf,[ $description,
2362 $money_char. $amount,
2369 $total->{'total_item'} = 'Sub-total';
2370 $total->{'total_amount'} =
2371 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2373 if ( $multisection ) {
2374 $tax_section->{'subtotal'} = $other_money_char.
2375 sprintf('%.2f', $taxtotal);
2376 $tax_section->{'pretotal'} = 'New charges sub-total '.
2377 $total->{'total_amount'};
2378 push @sections, $tax_section if $taxtotal;
2380 unshift @total_items, $total;
2383 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2385 push @buf,['','-----------'];
2386 push @buf,[( $conf->exists('disable_previous_balance')
2388 : 'Total New Charges'
2390 $money_char. sprintf("%10.2f",$self->charged) ];
2395 $total->{'total_item'} = &$embolden_function('Total');
2396 $total->{'total_amount'} =
2397 &$embolden_function(
2400 $self->charged + ( $conf->exists('disable_previous_balance')
2406 if ( $multisection ) {
2407 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2408 sprintf('%.2f', $self->charged );
2410 push @total_items, $total;
2412 push @buf,['','-----------'];
2413 push @buf,['Total Charges',
2415 sprintf( '%10.2f', $self->charged +
2416 ( $conf->exists('disable_previous_balance')
2425 unless ( $conf->exists('disable_previous_balance') ) {
2426 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2429 my $credittotal = 0;
2430 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2433 $total->{'total_item'} = &$escape_function($credit->{'description'});
2434 $credittotal += $credit->{'amount'};
2435 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2436 $adjusttotal += $credit->{'amount'};
2437 if ( $multisection ) {
2438 my $money = $old_latex ? '' : $money_char;
2439 push @detail_items, {
2440 ext_description => [],
2443 description => &$escape_function($credit->{'description'}),
2444 amount => $money. $credit->{'amount'},
2446 section => $adjust_section,
2449 push @total_items, $total;
2453 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2456 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2457 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2461 my $paymenttotal = 0;
2462 foreach my $payment ( $self->_items_payments ) {
2464 $total->{'total_item'} = &$escape_function($payment->{'description'});
2465 $paymenttotal += $payment->{'amount'};
2466 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2467 $adjusttotal += $payment->{'amount'};
2468 if ( $multisection ) {
2469 my $money = $old_latex ? '' : $money_char;
2470 push @detail_items, {
2471 ext_description => [],
2474 description => &$escape_function($payment->{'description'}),
2475 amount => $money. $payment->{'amount'},
2477 section => $adjust_section,
2480 push @total_items, $total;
2482 push @buf, [ $payment->{'description'},
2483 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2486 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2488 if ( $multisection ) {
2489 $adjust_section->{'subtotal'} = $other_money_char.
2490 sprintf('%.2f', $adjusttotal);
2491 push @sections, $adjust_section;
2496 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2497 $total->{'total_amount'} =
2498 &$embolden_function(
2499 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2501 if ( $multisection ) {
2502 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2503 $total->{'total_amount'};
2505 push @total_items, $total;
2507 push @buf,['','-----------'];
2508 push @buf,[$self->balance_due_msg, $money_char.
2509 sprintf("%10.2f", $balance_due ) ];
2513 if ( $multisection ) {
2514 push @sections, @$late_sections
2520 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2521 /invoice_lines\((\d*)\)/;
2522 $invoice_lines += $1 || scalar(@buf);
2525 die "no invoice_lines() functions in template?"
2526 if ( $format eq 'template' && !$wasfunc );
2528 if ($format eq 'template') {
2530 if ( $invoice_lines ) {
2531 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2532 $invoice_data{'total_pages'}++
2533 if scalar(@buf) % $invoice_lines;
2536 #setup subroutine for the template
2537 sub FS::cust_bill::_template::invoice_lines {
2538 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2540 scalar(@FS::cust_bill::_template::buf)
2541 ? shift @FS::cust_bill::_template::buf
2550 push @collect, split("\n",
2551 $text_template->fill_in( HASH => \%invoice_data,
2552 PACKAGE => 'FS::cust_bill::_template'
2555 $FS::cust_bill::_template::page++;
2557 map "$_\n", @collect;
2559 warn "filling in template for invoice ". $self->invnum. "\n"
2561 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2564 $text_template->fill_in(HASH => \%invoice_data);
2568 =item print_ps [ TIME [ , TEMPLATE ] ]
2570 Returns an postscript invoice, as a scalar.
2572 TIME an optional value used to control the printing of overdue messages. The
2573 default is now. It isn't the date of the invoice; that's the `_date' field.
2574 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2575 L<Time::Local> and L<Date::Parse> for conversion functions.
2582 my ($file, $lfile) = $self->print_latex(@_);
2583 my $ps = generate_ps($file);
2589 =item print_pdf [ TIME [ , TEMPLATE ] ]
2591 Returns an PDF invoice, as a scalar.
2593 TIME an optional value used to control the printing of overdue messages. The
2594 default is now. It isn't the date of the invoice; that's the `_date' field.
2595 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2596 L<Time::Local> and L<Date::Parse> for conversion functions.
2603 my ($file, $lfile) = $self->print_latex(@_);
2604 my $pdf = generate_pdf($file);
2610 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2612 Returns an HTML invoice, as a scalar.
2614 TIME an optional value used to control the printing of overdue messages. The
2615 default is now. It isn't the date of the invoice; that's the `_date' field.
2616 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2617 L<Time::Local> and L<Date::Parse> for conversion functions.
2619 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2620 when emailing the invoice as part of a multipart/related MIME email.
2628 %params = %{ shift() };
2630 $params{'time'} = shift;
2631 $params{'template'} = shift;
2632 $params{'cid'} = shift;
2635 $params{'format'} = 'html';
2637 $self->print_generic( %params );
2640 # quick subroutine for print_latex
2642 # There are ten characters that LaTeX treats as special characters, which
2643 # means that they do not simply typeset themselves:
2644 # # $ % & ~ _ ^ \ { }
2646 # TeX ignores blanks following an escaped character; if you want a blank (as
2647 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2651 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2652 $value =~ s/([<>])/\$$1\$/g;
2656 #utility methods for print_*
2658 sub _translate_old_latex_format {
2659 warn "_translate_old_latex_format called\n"
2666 if ( $line =~ /^%%Detail\s*$/ ) {
2668 push @template, q![@--!,
2669 q! foreach my $_tr_line (@detail_items) {!,
2670 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2671 q! $_tr_line->{'description'} .= !,
2672 q! "\\tabularnewline\n~~".!,
2673 q! join( "\\tabularnewline\n~~",!,
2674 q! @{$_tr_line->{'ext_description'}}!,
2678 while ( ( my $line_item_line = shift )
2679 !~ /^%%EndDetail\s*$/ ) {
2680 $line_item_line =~ s/'/\\'/g; # nice LTS
2681 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2682 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2683 push @template, " \$OUT .= '$line_item_line';";
2686 push @template, '}',
2689 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2691 push @template, '[@--',
2692 ' foreach my $_tr_line (@total_items) {';
2694 while ( ( my $total_item_line = shift )
2695 !~ /^%%EndTotalDetails\s*$/ ) {
2696 $total_item_line =~ s/'/\\'/g; # nice LTS
2697 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2698 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2699 push @template, " \$OUT .= '$total_item_line';";
2702 push @template, '}',
2706 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2707 push @template, $line;
2713 warn "$_\n" foreach @template;
2722 #check for an invoice- specific override (eventually)
2724 #check for a customer- specific override
2725 return $self->cust_main->invoice_terms
2726 if $self->cust_main->invoice_terms;
2728 #use configured default
2729 $conf->config('invoice_default_terms') || '';
2735 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2736 $duedate = $self->_date() + ( $1 * 86400 );
2743 $self->due_date ? time2str(shift, $self->due_date) : '';
2746 sub balance_due_msg {
2748 my $msg = 'Balance Due';
2749 return $msg unless $self->terms;
2750 if ( $self->due_date ) {
2751 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2752 } elsif ( $self->terms ) {
2753 $msg .= ' - '. $self->terms;
2758 sub balance_due_date {
2761 if ( $conf->exists('invoice_default_terms')
2762 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2763 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2768 =item invnum_date_pretty
2770 Returns a string with the invoice number and date, for example:
2771 "Invoice #54 (3/20/2008)"
2775 sub invnum_date_pretty {
2777 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2782 Returns a string with the date, for example: "3/20/2008"
2788 time2str('%x', $self->_date);
2791 sub _items_sections {
2798 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2801 if ( $cust_bill_pkg->pkgnum > 0 ) {
2802 my $usage = $cust_bill_pkg->usage;
2804 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2805 my $desc = $display->section;
2806 my $type = $display->type;
2808 if ( $display->post_total ) {
2809 if (! $type || $type eq 'S') {
2810 $l{$desc} += $cust_bill_pkg->setup
2811 if ( $cust_bill_pkg->setup != 0 );
2815 $l{$desc} += $cust_bill_pkg->recur
2816 if ( $cust_bill_pkg->recur != 0 );
2819 if ($type && $type eq 'R') {
2820 $l{$desc} += $cust_bill_pkg->recur - $usage
2821 if ( $cust_bill_pkg->recur != 0 );
2824 if ($type && $type eq 'U') {
2825 $l{$desc} += $usage;
2829 if (! $type || $type eq 'S') {
2830 $s{$desc} += $cust_bill_pkg->setup
2831 if ( $cust_bill_pkg->setup != 0 );
2835 $s{$desc} += $cust_bill_pkg->recur
2836 if ( $cust_bill_pkg->recur != 0 );
2839 if ($type && $type eq 'R') {
2840 $s{$desc} += $cust_bill_pkg->recur - $usage
2841 if ( $cust_bill_pkg->recur != 0 );
2844 if ($type && $type eq 'U') {
2845 $s{$desc} += $usage;
2856 push @$late, map { { 'description' => $_,
2857 'subtotal' => $l{$_},
2861 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2868 #my @display = scalar(@_)
2870 # : qw( _items_previous _items_pkg );
2871 # #: qw( _items_pkg );
2872 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2873 my @display = qw( _items_previous _items_pkg );
2876 foreach my $display ( @display ) {
2877 push @b, $self->$display(@_);
2882 sub _items_previous {
2884 my $cust_main = $self->cust_main;
2885 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2887 foreach ( @pr_cust_bill ) {
2889 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2890 ' ('. time2str('%x',$_->_date). ')',
2891 #'pkgpart' => 'N/A',
2893 'amount' => sprintf("%.2f", $_->owed),
2899 # 'description' => 'Previous Balance',
2900 # #'pkgpart' => 'N/A',
2901 # 'pkgnum' => 'N/A',
2902 # 'amount' => sprintf("%10.2f", $pr_total ),
2903 # 'ext_description' => [ map {
2904 # "Invoice ". $_->invnum.
2905 # " (". time2str("%x",$_->_date). ") ".
2906 # sprintf("%10.2f", $_->owed)
2907 # } @pr_cust_bill ],
2914 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2915 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2919 return 0 unless $a cmp $b;
2920 return -1 if $b eq 'Tax';
2921 return 1 if $a eq 'Tax';
2922 return -1 if $b eq 'Other surcharges';
2923 return 1 if $a eq 'Other surcharges';
2929 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2930 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2933 sub _items_cust_bill_pkg {
2935 my $cust_bill_pkg = shift;
2938 my $format = $opt{format} || '';
2939 my $escape_function = $opt{escape_function} || sub { shift };
2940 my $format_function = $opt{format_function} || '';
2941 my $unsquelched = $opt{unsquelched} || '';
2942 my $section = $opt{section}->{description} if $opt{section};
2945 my ($s, $r, $u) = ( undef, undef, undef );
2946 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2949 foreach ( $s, $r, $u ) {
2950 if ( $_ && !$cust_bill_pkg->hidden ) {
2951 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2952 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2958 foreach my $display ( grep { defined($section)
2959 ? $_->section eq $section
2962 $cust_bill_pkg->cust_bill_pkg_display
2966 my $type = $display->type;
2968 my $desc = $cust_bill_pkg->desc;
2969 $desc = substr($desc, 0, 50). '...'
2970 if $format eq 'latex' && length($desc) > 50;
2972 my %details_opt = ( 'format' => $format,
2973 'escape_function' => $escape_function,
2974 'format_function' => $format_function,
2977 if ( $cust_bill_pkg->pkgnum > 0 ) {
2979 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2981 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2983 my $description = $desc;
2984 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2987 push @d, map &{$escape_function}($_),
2988 $cust_pkg->h_labels_short($self->_date)
2989 unless $cust_pkg->part_pkg->hide_svc_detail
2990 || $cust_bill_pkg->hidden;
2991 push @d, $cust_bill_pkg->details(%details_opt)
2992 if $cust_bill_pkg->recur == 0;
2994 if ( $cust_bill_pkg->hidden ) {
2995 $s->{amount} += $cust_bill_pkg->setup;
2996 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2997 push @{ $s->{ext_description} }, @d;
3000 description => $description,
3001 #pkgpart => $part_pkg->pkgpart,
3002 pkgnum => $cust_bill_pkg->pkgnum,
3003 amount => $cust_bill_pkg->setup,
3004 unit_amount => $cust_bill_pkg->unitsetup,
3005 quantity => $cust_bill_pkg->quantity,
3006 ext_description => \@d,
3012 if ( $cust_bill_pkg->recur != 0 &&
3013 ( !$type || $type eq 'R' || $type eq 'U' )
3017 my $is_summary = $display->summary;
3018 my $description = $is_summary ? "Usage charges" : $desc;
3020 unless ( $conf->exists('disable_line_item_date_ranges') ) {
3021 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3022 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3027 #at least until cust_bill_pkg has "past" ranges in addition to
3028 #the "future" sdate/edate ones... see #3032
3029 my @dates = ( $self->_date );
3030 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3031 push @dates, $prev->sdate if $prev;
3033 push @d, map &{$escape_function}($_),
3034 $cust_pkg->h_labels_short(@dates)
3035 #$cust_bill_pkg->edate,
3036 #$cust_bill_pkg->sdate)
3037 unless $cust_pkg->part_pkg->hide_svc_detail
3038 || $cust_bill_pkg->itemdesc
3039 || $cust_bill_pkg->hidden
3042 push @d, $cust_bill_pkg->details(%details_opt)
3043 unless ($is_summary || $type && $type eq 'R');
3047 $amount = $cust_bill_pkg->recur;
3048 }elsif($type eq 'R') {
3049 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3050 }elsif($type eq 'U') {
3051 $amount = $cust_bill_pkg->usage;
3054 if ( !$type || $type eq 'R' ) {
3056 if ( $cust_bill_pkg->hidden ) {
3057 $r->{amount} += $amount;
3058 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3059 push @{ $r->{ext_description} }, @d;
3062 description => $description,
3063 #pkgpart => $part_pkg->pkgpart,
3064 pkgnum => $cust_bill_pkg->pkgnum,
3066 unit_amount => $cust_bill_pkg->unitrecur,
3067 quantity => $cust_bill_pkg->quantity,
3068 ext_description => \@d,
3072 } elsif ( $amount ) { # && $type eq 'U'
3074 if ( $cust_bill_pkg->hidden ) {
3075 $u->{amount} += $amount;
3076 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3077 push @{ $u->{ext_description} }, @d;
3080 description => $description,
3081 #pkgpart => $part_pkg->pkgpart,
3082 pkgnum => $cust_bill_pkg->pkgnum,
3084 unit_amount => $cust_bill_pkg->unitrecur,
3085 quantity => $cust_bill_pkg->quantity,
3086 ext_description => \@d,
3092 } # recurring or usage with recurring charge
3094 } else { #pkgnum tax or one-shot line item (??)
3096 if ( $cust_bill_pkg->setup != 0 ) {
3098 'description' => $desc,
3099 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3102 if ( $cust_bill_pkg->recur != 0 ) {
3104 'description' => "$desc (".
3105 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3106 time2str("%x", $cust_bill_pkg->edate). ')',
3107 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3117 foreach ( $s, $r, $u ) {
3119 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3120 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3129 sub _items_credits {
3130 my( $self, %opt ) = @_;
3131 my $trim_len = $opt{'trim_len'} || 60;
3135 foreach ( $self->cust_credited ) {
3137 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3139 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3140 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3141 $reason = " ($reason) " if $reason;
3144 #'description' => 'Credit ref\#'. $_->crednum.
3145 # " (". time2str("%x",$_->cust_credit->_date) .")".
3147 'description' => 'Credit applied '.
3148 time2str("%x",$_->cust_credit->_date). $reason,
3149 'amount' => sprintf("%.2f",$_->amount),
3157 sub _items_payments {
3161 #get & print payments
3162 foreach ( $self->cust_bill_pay ) {
3164 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3167 'description' => "Payment received ".
3168 time2str("%x",$_->cust_pay->_date ),
3169 'amount' => sprintf("%.2f", $_->amount )
3177 =item call_details [ OPTION => VALUE ... ]
3179 Returns an array of CSV strings representing the call details for this invoice
3180 The only option available is the boolean prepend_billed_number
3185 my ($self, %opt) = @_;
3187 my $format_function = sub { shift };
3189 if ($opt{prepend_billed_number}) {
3190 $format_function = sub {
3194 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3199 my @details = map { $_->details( 'format_function' => $format_function,
3200 'escape_function' => sub{ return() },
3204 $self->cust_bill_pkg;
3205 my $header = $details[0];
3206 ( $header, grep { $_ ne $header } @details );
3216 =item process_reprint
3220 sub process_reprint {
3221 process_re_X('print', @_);
3224 =item process_reemail
3228 sub process_reemail {
3229 process_re_X('email', @_);
3237 process_re_X('fax', @_);
3245 process_re_X('ftp', @_);
3252 sub process_respool {
3253 process_re_X('spool', @_);
3256 use Storable qw(thaw);
3260 my( $method, $job ) = ( shift, shift );
3261 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3263 my $param = thaw(decode_base64(shift));
3264 warn Dumper($param) if $DEBUG;
3275 my($method, $job, %param ) = @_;
3277 warn "re_X $method for job $job with param:\n".
3278 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3281 #some false laziness w/search/cust_bill.html
3283 my $orderby = 'ORDER BY cust_bill._date';
3285 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3287 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3289 my @cust_bill = qsearch( {
3290 #'select' => "cust_bill.*",
3291 'table' => 'cust_bill',
3292 'addl_from' => $addl_from,
3294 'extra_sql' => $extra_sql,
3295 'order_by' => $orderby,
3299 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3301 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3304 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3305 foreach my $cust_bill ( @cust_bill ) {
3306 $cust_bill->$method();
3308 if ( $job ) { #progressbar foo
3310 if ( time - $min_sec > $last ) {
3311 my $error = $job->update_statustext(
3312 int( 100 * $num / scalar(@cust_bill) )
3314 die $error if $error;
3325 =head1 CLASS METHODS
3331 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3337 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3342 Returns an SQL fragment to retreive the net amount (charged minus credited).
3348 'charged - '. $class->credited_sql;
3353 Returns an SQL fragment to retreive the amount paid against this invoice.
3359 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3360 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3365 Returns an SQL fragment to retreive the amount credited against this invoice.
3371 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3372 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3375 =item search_sql HASHREF
3377 Class method which returns an SQL WHERE fragment to search for parameters
3378 specified in HASHREF. Valid parameters are
3384 Epoch date (UNIX timestamp) setting a lower bound for _date values
3388 Epoch date (UNIX timestamp) setting an upper bound for _date values
3402 =item newest_percust
3406 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3411 my($class, $param) = @_;
3413 warn "$me search_sql called with params: \n".
3414 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3419 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3420 push @search, "cust_bill._date >= $1";
3422 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3423 push @search, "cust_bill._date < $1";
3425 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3426 push @search, "cust_bill.invnum >= $1";
3428 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3429 push @search, "cust_bill.invnum <= $1";
3431 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3432 push @search, "cust_main.agentnum = $1";
3435 push @search, '0 != '. FS::cust_bill->owed_sql
3436 if $param->{'open'};
3438 push @search, '0 != '. FS::cust_bill->net_sql
3441 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3442 if $param->{'days'};
3444 if ( $param->{'newest_percust'} ) {
3446 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3447 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3449 my @newest_where = map { my $x = $_;
3450 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3453 grep ! /^cust_main./, @search;
3454 my $newest_where = scalar(@newest_where)
3455 ? ' AND '. join(' AND ', @newest_where)
3459 push @search, "cust_bill._date = (
3460 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3461 WHERE newest_cust_bill.custnum = cust_bill.custnum
3467 my $curuser = $FS::CurrentUser::CurrentUser;
3468 if ( $curuser->username eq 'fs_queue'
3469 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3471 my $newuser = qsearchs('access_user', {
3472 'username' => $username,
3476 $curuser = $newuser;
3478 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3482 push @search, $curuser->agentnums_sql;
3484 join(' AND ', @search );
3496 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3497 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base