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_bill_pkg;
20 use FS::cust_bill_pkg_display;
24 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
30 use FS::cust_bill_pay;
31 use FS::cust_bill_pay_batch;
32 use FS::part_bill_event;
35 @ISA = qw( FS::cust_main_Mixin FS::Record );
38 $me = '[FS::cust_bill]';
40 #ask FS::UID to run this stuff for us later
41 FS::UID->install_callback( sub {
43 $money_char = $conf->config('money_char') || '$';
48 FS::cust_bill - Object methods for cust_bill records
54 $record = new FS::cust_bill \%hash;
55 $record = new FS::cust_bill { 'column' => 'value' };
57 $error = $record->insert;
59 $error = $new_record->replace($old_record);
61 $error = $record->delete;
63 $error = $record->check;
65 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
67 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
69 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
71 @cust_pay_objects = $cust_bill->cust_pay;
73 $tax_amount = $record->tax;
75 @lines = $cust_bill->print_text;
76 @lines = $cust_bill->print_text $time;
80 An FS::cust_bill object represents an invoice; a declaration that a customer
81 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
82 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
83 following fields are currently supported:
87 =item invnum - primary key (assigned automatically for new invoices)
89 =item custnum - customer (see L<FS::cust_main>)
91 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
92 L<Time::Local> and L<Date::Parse> for conversion functions.
94 =item charged - amount of this invoice
96 =item printed - deprecated
98 =item closed - books closed flag, empty or `Y'
108 Creates a new invoice. To add the invoice to the database, see L<"insert">.
109 Invoices are normally created by calling the bill method of a customer object
110 (see L<FS::cust_main>).
114 sub table { 'cust_bill'; }
116 sub cust_linked { $_[0]->cust_main_custnum; }
117 sub cust_unlinked_msg {
119 "WARNING: can't find cust_main.custnum ". $self->custnum.
120 ' (cust_bill.invnum '. $self->invnum. ')';
125 Adds this invoice to the database ("Posts" the invoice). If there is an error,
126 returns the error, otherwise returns false.
130 This method now works but you probably shouldn't use it. Instead, apply a
131 credit against the invoice.
133 Using this method to delete invoices outright is really, really bad. There
134 would be no record you ever posted this invoice, and there are no check to
135 make sure charged = 0 or that there are no associated cust_bill_pkg records.
137 Really, don't use it.
143 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
144 $self->SUPER::delete(@_);
147 =item replace OLD_RECORD
149 Replaces the OLD_RECORD with this one in the database. If there is an error,
150 returns the error, otherwise returns false.
152 Only printed may be changed. printed is normally updated by calling the
153 collect method of a customer object (see L<FS::cust_main>).
157 #replace can be inherited from Record.pm
159 # replace_check is now the preferred way to #implement replace data checks
160 # (so $object->replace() works without an argument)
163 my( $new, $old ) = ( shift, shift );
164 return "Can't change custnum!" unless $old->custnum == $new->custnum;
165 #return "Can't change _date!" unless $old->_date eq $new->_date;
166 return "Can't change _date!" unless $old->_date == $new->_date;
167 return "Can't change charged!" unless $old->charged == $new->charged
168 || $old->charged == 0;
175 Checks all fields to make sure this is a valid invoice. If there is an error,
176 returns the error, otherwise returns false. Called by the insert and replace
185 $self->ut_numbern('invnum')
186 || $self->ut_number('custnum')
187 || $self->ut_numbern('_date')
188 || $self->ut_money('charged')
189 || $self->ut_numbern('printed')
190 || $self->ut_enum('closed', [ '', 'Y' ])
192 return $error if $error;
194 return "Unknown customer"
195 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
197 $self->_date(time) unless $self->_date;
199 $self->printed(0) if $self->printed eq '';
206 Returns a list consisting of the total previous balance for this customer,
207 followed by the previous outstanding invoices (as FS::cust_bill objects also).
214 my @cust_bill = sort { $a->_date <=> $b->_date }
215 grep { $_->owed != 0 && $_->_date < $self->_date }
216 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
218 foreach ( @cust_bill ) { $total += $_->owed; }
224 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
231 { 'table' => 'cust_bill_pkg',
232 'hashref' => { 'invnum' => $self->invnum },
233 'order_by' => 'ORDER BY billpkgnum',
238 =item cust_bill_pkg_pkgnum PKGNUM
240 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
245 sub cust_bill_pkg_pkgnum {
246 my( $self, $pkgnum ) = @_;
248 { 'table' => 'cust_bill_pkg',
249 'hashref' => { 'invnum' => $self->invnum,
252 'order_by' => 'ORDER BY billpkgnum',
259 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
266 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
268 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
271 =item open_cust_bill_pkg
273 Returns the open line items for this invoice.
275 Note that cust_bill_pkg with both setup and recur fees are returned as two
276 separate line items, each with only one fee.
280 # modeled after cust_main::open_cust_bill
281 sub open_cust_bill_pkg {
284 # grep { $_->owed > 0 } $self->cust_bill_pkg
286 my %other = ( 'recur' => 'setup',
287 'setup' => 'recur', );
289 foreach my $field ( qw( recur setup )) {
290 push @open, map { $_->set( $other{$field}, 0 ); $_; }
291 grep { $_->owed($field) > 0 }
292 $self->cust_bill_pkg;
298 =item cust_bill_event
300 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
304 sub cust_bill_event {
306 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
309 =item num_cust_bill_event
311 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
315 sub num_cust_bill_event {
318 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
319 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
320 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
321 $sth->fetchrow_arrayref->[0];
326 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
330 #false laziness w/cust_pkg.pm
334 'table' => 'cust_event',
335 'addl_from' => 'JOIN part_event USING ( eventpart )',
336 'hashref' => { 'tablenum' => $self->invnum },
337 'extra_sql' => " AND eventtable = 'cust_bill' ",
343 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
347 #false laziness w/cust_pkg.pm
351 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
352 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
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 customer (see L<FS::cust_main>) for this invoice.
366 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
369 =item cust_suspend_if_balance_over AMOUNT
371 Suspends the customer associated with this invoice if the total amount owed on
372 this invoice and all older invoices is greater than the specified amount.
374 Returns a list: an empty list on success or a list of errors.
378 sub cust_suspend_if_balance_over {
379 my( $self, $amount ) = ( shift, shift );
380 my $cust_main = $self->cust_main;
381 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
384 $cust_main->suspend(@_);
390 Depreciated. See the cust_credited method.
392 #Returns a list consisting of the total previous credited (see
393 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
394 #outstanding credits (FS::cust_credit objects).
400 croak "FS::cust_bill->cust_credit depreciated; see ".
401 "FS::cust_bill->cust_credit_bill";
404 #my @cust_credit = sort { $a->_date <=> $b->_date }
405 # grep { $_->credited != 0 && $_->_date < $self->_date }
406 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
408 #foreach (@cust_credit) { $total += $_->credited; }
409 #$total, @cust_credit;
414 Depreciated. See the cust_bill_pay method.
416 #Returns all payments (see L<FS::cust_pay>) for this invoice.
422 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
424 #sort { $a->_date <=> $b->_date }
425 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
431 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
437 sort { $a->_date <=> $b->_date }
438 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
443 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
449 sort { $a->_date <=> $b->_date }
450 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
454 =item cust_bill_pay_pkgnum PKGNUM
456 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
457 with matching pkgnum.
461 sub cust_bill_pay_pkgnum {
462 my( $self, $pkgnum ) = @_;
463 sort { $a->_date <=> $b->_date }
464 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
470 =item cust_credited_pkgnum PKGNUM
472 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
473 with matching pkgnum.
477 sub cust_credited_pkgnum {
478 my( $self, $pkgnum ) = @_;
479 sort { $a->_date <=> $b->_date }
480 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
488 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
495 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
497 foreach (@taxlines) { $total += $_->setup; }
503 Returns the amount owed (still outstanding) on this invoice, which is charged
504 minus all payment applications (see L<FS::cust_bill_pay>) and credit
505 applications (see L<FS::cust_credit_bill>).
511 my $balance = $self->charged;
512 $balance -= $_->amount foreach ( $self->cust_bill_pay );
513 $balance -= $_->amount foreach ( $self->cust_credited );
514 $balance = sprintf( "%.2f", $balance);
515 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
520 my( $self, $pkgnum ) = @_;
522 #my $balance = $self->charged;
524 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
526 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
527 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
529 $balance = sprintf( "%.2f", $balance);
530 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
534 =item apply_payments_and_credits
538 sub apply_payments_and_credits {
541 local $SIG{HUP} = 'IGNORE';
542 local $SIG{INT} = 'IGNORE';
543 local $SIG{QUIT} = 'IGNORE';
544 local $SIG{TERM} = 'IGNORE';
545 local $SIG{TSTP} = 'IGNORE';
546 local $SIG{PIPE} = 'IGNORE';
548 my $oldAutoCommit = $FS::UID::AutoCommit;
549 local $FS::UID::AutoCommit = 0;
552 $self->select_for_update; #mutex
554 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
555 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
557 if ( $conf->exists('pkg-balances') ) {
558 # limit @payments & @credits to those w/ a pkgnum grepped from $self
559 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
560 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
561 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
564 while ( $self->owed > 0 and ( @payments || @credits ) ) {
567 if ( @payments && @credits ) {
569 #decide which goes first by weight of top (unapplied) line item
571 my @open_lineitems = $self->open_cust_bill_pkg;
574 max( map { $_->part_pkg->pay_weight || 0 }
579 my $max_credit_weight =
580 max( map { $_->part_pkg->credit_weight || 0 }
586 #if both are the same... payments first? it has to be something
587 if ( $max_pay_weight >= $max_credit_weight ) {
593 } elsif ( @payments ) {
595 } elsif ( @credits ) {
598 die "guru meditation #12 and 35";
602 if ( $app eq 'pay' ) {
604 my $payment = shift @payments;
605 $unapp_amount = $payment->unapplied;
606 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
607 $app->pkgnum( $payment->pkgnum )
608 if $conf->exists('pkg-balances') && $payment->pkgnum;
610 } elsif ( $app eq 'credit' ) {
612 my $credit = shift @credits;
613 $unapp_amount = $credit->credited;
614 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
615 $app->pkgnum( $credit->pkgnum )
616 if $conf->exists('pkg-balances') && $credit->pkgnum;
619 die "guru meditation #12 and 35";
623 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
624 warn "owed_pkgnum ". $app->pkgnum;
625 $owed = $self->owed_pkgnum($app->pkgnum);
629 next unless $owed > 0;
631 warn "min ( $unapp_amount, $owed )\n";
632 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
634 $app->invnum( $self->invnum );
636 my $error = $app->insert;
638 $dbh->rollback if $oldAutoCommit;
639 return "Error inserting ". $app->table. " record: $error";
641 die $error if $error;
645 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
650 =item generate_email OPTION => VALUE ...
658 sender address, required
662 alternate template name, optional
666 text attachment arrayref, optional
670 email subject, optional
674 Returns an argument list to be passed to L<FS::Misc::send_email>.
685 my $me = '[FS::cust_bill::generate_email]';
688 'from' => $args{'from'},
689 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
692 my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
694 if (ref($args{'to'}) eq 'ARRAY') {
695 $return{'to'} = $args{'to'};
697 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
698 $self->cust_main->invoicing_list
702 if ( $conf->exists('invoice_html') ) {
704 warn "$me creating HTML/text multipart message"
707 $return{'nobody'} = 1;
709 my $alternative = build MIME::Entity
710 'Type' => 'multipart/alternative',
711 'Encoding' => '7bit',
712 'Disposition' => 'inline'
716 if ( $conf->exists('invoice_email_pdf')
717 and scalar($conf->config('invoice_email_pdf_note')) ) {
719 warn "$me using 'invoice_email_pdf_note' in multipart message"
721 $data = [ map { $_ . "\n" }
722 $conf->config('invoice_email_pdf_note')
727 warn "$me not using 'invoice_email_pdf_note' in multipart message"
729 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
730 $data = $args{'print_text'};
732 $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
737 $alternative->attach(
738 'Type' => 'text/plain',
739 #'Encoding' => 'quoted-printable',
740 'Encoding' => '7bit',
742 'Disposition' => 'inline',
745 $args{'from'} =~ /\@([\w\.\-]+)/;
746 my $from = $1 || 'example.com';
747 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
750 my $agentnum = $self->cust_main->agentnum;
751 if ( defined($args{'template'}) && length($args{'template'})
752 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
755 $logo = 'logo_'. $args{'template'}. '.png';
759 my $image_data = $conf->config_binary( $logo, $agentnum);
761 my $image = build MIME::Entity
762 'Type' => 'image/png',
763 'Encoding' => 'base64',
764 'Data' => $image_data,
765 'Filename' => 'logo.png',
766 'Content-ID' => "<$content_id>",
769 $alternative->attach(
770 'Type' => 'text/html',
771 'Encoding' => 'quoted-printable',
772 'Data' => [ '<html>',
775 ' '. encode_entities($return{'subject'}),
778 ' <body bgcolor="#e8e8e8">',
779 $self->print_html({ time => '',
780 template => $args{'template'},
787 'Disposition' => 'inline',
788 #'Filename' => 'invoice.pdf',
792 if ( $self->cust_main->email_csv_cdr ) {
794 push @otherparts, build MIME::Entity
795 'Type' => 'text/csv',
796 'Encoding' => '7bit',
797 'Data' => [ map { "$_\n" }
798 $self->call_details('prepend_billed_number' => 1)
800 'Disposition' => 'attachment',
801 'Filename' => 'usage-'. $self->invnum. '.csv',
806 if ( $conf->exists('invoice_email_pdf') ) {
811 # multipart/alternative
817 my $related = build MIME::Entity 'Type' => 'multipart/related',
818 'Encoding' => '7bit';
820 #false laziness w/Misc::send_email
821 $related->head->replace('Content-type',
823 '; boundary="'. $related->head->multipart_boundary. '"'.
824 '; type=multipart/alternative'
827 $related->add_part($alternative);
829 $related->add_part($image);
831 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
833 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
837 #no other attachment:
839 # multipart/alternative
844 $return{'content-type'} = 'multipart/related';
845 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
846 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
847 #$return{'disposition'} = 'inline';
853 if ( $conf->exists('invoice_email_pdf') ) {
854 warn "$me creating PDF attachment"
857 #mime parts arguments a la MIME::Entity->build().
858 $return{'mimeparts'} = [
859 { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
863 if ( $conf->exists('invoice_email_pdf')
864 and scalar($conf->config('invoice_email_pdf_note')) ) {
866 warn "$me using 'invoice_email_pdf_note'"
868 $return{'body'} = [ map { $_ . "\n" }
869 $conf->config('invoice_email_pdf_note')
874 warn "$me not using 'invoice_email_pdf_note'"
876 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
877 $return{'body'} = $args{'print_text'};
879 $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
892 Returns a list suitable for passing to MIME::Entity->build(), representing
893 this invoice as PDF attachment.
900 'Type' => 'application/pdf',
901 'Encoding' => 'base64',
902 'Data' => [ $self->print_pdf(@_) ],
903 'Disposition' => 'attachment',
904 'Filename' => 'invoice-'. $self->invnum. '.pdf',
908 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
910 Sends this invoice to the destinations configured for this customer: sends
911 email, prints and/or faxes. See L<FS::cust_main_invoice>.
913 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
915 AGENTNUM, if specified, means that this invoice will only be sent for customers
916 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
917 single agent) or an arrayref of agentnums.
919 INVOICE_FROM, if specified, overrides the default email invoice From: address.
921 AMOUNT, if specified, only sends the invoice if the total amount owed on this
922 invoice and all older invoices is greater than the specified amount.
929 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
930 or die "invalid invoice number: " . $opt{invnum};
932 my @args = ( $opt{template}, $opt{agentnum} );
933 push @args, $opt{invoice_from}
934 if exists($opt{invoice_from}) && $opt{invoice_from};
936 my $error = $self->send( @args );
937 die $error if $error;
943 my $template = scalar(@_) ? shift : '';
944 if ( scalar(@_) && $_[0] ) {
945 my $agentnums = ref($_[0]) ? shift : [ shift ];
946 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
952 : ( $self->_agent_invoice_from || #XXX should go away
953 $conf->config('invoice_from', $self->cust_main->agentnum )
956 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
959 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
961 my @invoicing_list = $self->cust_main->invoicing_list;
963 #$self->email_invoice($template, $invoice_from)
964 $self->email($template, $invoice_from)
965 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
967 #$self->print_invoice($template)
968 $self->print($template)
969 if grep { $_ eq 'POST' } @invoicing_list; #postal
971 $self->fax_invoice($template)
972 if grep { $_ eq 'FAX' } @invoicing_list; #fax
978 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
982 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
984 INVOICE_FROM, if specified, overrides the default email invoice From: address.
988 sub queueable_email {
991 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
992 or die "invalid invoice number: " . $opt{invnum};
994 my @args = ( $opt{template} );
995 push @args, $opt{invoice_from}
996 if exists($opt{invoice_from}) && $opt{invoice_from};
998 my $error = $self->email( @args );
999 die $error if $error;
1003 #sub email_invoice {
1006 my $template = scalar(@_) ? shift : '';
1010 : ( $self->_agent_invoice_from || #XXX should go away
1011 $conf->config('invoice_from', $self->cust_main->agentnum )
1015 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1016 $self->cust_main->invoicing_list;
1018 #better to notify this person than silence
1019 @invoicing_list = ($invoice_from) unless @invoicing_list;
1021 my $subject = $self->email_subject($template);
1023 my $error = send_email(
1024 $self->generate_email(
1025 'from' => $invoice_from,
1026 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1027 'subject' => $subject,
1028 'template' => $template,
1031 die "can't email invoice: $error\n" if $error;
1032 #die "$error\n" if $error;
1039 #my $template = scalar(@_) ? shift : '';
1042 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1045 my $cust_main = $self->cust_main;
1046 my $name = $cust_main->name;
1047 my $name_short = $cust_main->name_short;
1048 my $invoice_number = $self->invnum;
1049 my $invoice_date = $self->_date_pretty;
1051 eval qq("$subject");
1054 =item lpr_data [ TEMPLATENAME ]
1056 Returns the postscript or plaintext for this invoice as an arrayref.
1058 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1063 my( $self, $template) = @_;
1064 $conf->exists('invoice_latex')
1065 ? [ $self->print_ps('', $template) ]
1066 : [ $self->print_text('', $template) ];
1069 =item print [ TEMPLATENAME ]
1071 Prints this invoice.
1073 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1077 #sub print_invoice {
1080 my $template = scalar(@_) ? shift : '';
1082 do_print $self->lpr_data($template);
1085 =item fax_invoice [ TEMPLATENAME ]
1089 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1095 my $template = scalar(@_) ? shift : '';
1097 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1098 unless $conf->exists('invoice_latex');
1100 my $dialstring = $self->cust_main->getfield('fax');
1103 my $error = send_fax( 'docdata' => $self->lpr_data($template),
1104 'dialstring' => $dialstring,
1106 die $error if $error;
1110 =item ftp_invoice [ TEMPLATENAME ]
1112 Sends this invoice data via FTP.
1114 TEMPLATENAME is unused?
1120 my $template = scalar(@_) ? shift : '';
1123 'protocol' => 'ftp',
1124 'server' => $conf->config('cust_bill-ftpserver'),
1125 'username' => $conf->config('cust_bill-ftpusername'),
1126 'password' => $conf->config('cust_bill-ftppassword'),
1127 'dir' => $conf->config('cust_bill-ftpdir'),
1128 'format' => $conf->config('cust_bill-ftpformat'),
1132 =item spool_invoice [ TEMPLATENAME ]
1134 Spools this invoice data (see L<FS::spool_csv>)
1136 TEMPLATENAME is unused?
1142 my $template = scalar(@_) ? shift : '';
1145 'format' => $conf->config('cust_bill-spoolformat'),
1146 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1150 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1152 Like B<send>, but only sends the invoice if it is the newest open invoice for
1157 sub send_if_newest {
1162 grep { $_->owed > 0 }
1163 qsearch('cust_bill', {
1164 'custnum' => $self->custnum,
1165 #'_date' => { op=>'>', value=>$self->_date },
1166 'invnum' => { op=>'>', value=>$self->invnum },
1173 =item send_csv OPTION => VALUE, ...
1175 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1179 protocol - currently only "ftp"
1185 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1186 and YYMMDDHHMMSS is a timestamp.
1188 See L</print_csv> for a description of the output format.
1193 my($self, %opt) = @_;
1197 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1198 mkdir $spooldir, 0700 unless -d $spooldir;
1200 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1201 my $file = "$spooldir/$tracctnum.csv";
1203 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1205 open(CSV, ">$file") or die "can't open $file: $!";
1213 if ( $opt{protocol} eq 'ftp' ) {
1214 eval "use Net::FTP;";
1216 $net = Net::FTP->new($opt{server}) or die @$;
1218 die "unknown protocol: $opt{protocol}";
1221 $net->login( $opt{username}, $opt{password} )
1222 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1224 $net->binary or die "can't set binary mode";
1226 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1228 $net->put($file) or die "can't put $file: $!";
1238 Spools CSV invoice data.
1244 =item format - 'default' or 'billco'
1246 =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>).
1248 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1250 =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.
1257 my($self, %opt) = @_;
1259 my $cust_main = $self->cust_main;
1261 if ( $opt{'dest'} ) {
1262 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1263 $cust_main->invoicing_list;
1264 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1265 || ! keys %invoicing_list;
1268 if ( $opt{'balanceover'} ) {
1270 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1273 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1274 mkdir $spooldir, 0700 unless -d $spooldir;
1276 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1280 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1281 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1284 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1286 open(CSV, ">>$file") or die "can't open $file: $!";
1287 flock(CSV, LOCK_EX);
1292 if ( lc($opt{'format'}) eq 'billco' ) {
1294 flock(CSV, LOCK_UN);
1299 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1302 open(CSV,">>$file") or die "can't open $file: $!";
1303 flock(CSV, LOCK_EX);
1309 flock(CSV, LOCK_UN);
1316 =item print_csv OPTION => VALUE, ...
1318 Returns CSV data for this invoice.
1322 format - 'default' or 'billco'
1324 Returns a list consisting of two scalars. The first is a single line of CSV
1325 header information for this invoice. The second is one or more lines of CSV
1326 detail information for this invoice.
1328 If I<format> is not specified or "default", the fields of the CSV file are as
1331 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1335 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1337 B<record_type> is C<cust_bill> for the initial header line only. The
1338 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1339 fields are filled in.
1341 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1342 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1345 =item invnum - invoice number
1347 =item custnum - customer number
1349 =item _date - invoice date
1351 =item charged - total invoice amount
1353 =item first - customer first name
1355 =item last - customer first name
1357 =item company - company name
1359 =item address1 - address line 1
1361 =item address2 - address line 1
1371 =item pkg - line item description
1373 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1375 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1377 =item sdate - start date for recurring fee
1379 =item edate - end date for recurring fee
1383 If I<format> is "billco", the fields of the header CSV file are as follows:
1385 +-------------------------------------------------------------------+
1386 | FORMAT HEADER FILE |
1387 |-------------------------------------------------------------------|
1388 | Field | Description | Name | Type | Width |
1389 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1390 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1391 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1392 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1393 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1394 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1395 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1396 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1397 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1398 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1399 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1400 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1401 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1402 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1403 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1404 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1405 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1406 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1407 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1408 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1409 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1410 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1411 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1412 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1413 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1414 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1415 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1416 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1417 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1418 +-------+-------------------------------+------------+------+-------+
1420 If I<format> is "billco", the fields of the detail CSV file are as follows:
1422 FORMAT FOR DETAIL FILE
1424 Field | Description | Name | Type | Width
1425 1 | N/A-Leave Empty | RC | CHAR | 2
1426 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1427 3 | Account Number | TRACCTNUM | CHAR | 15
1428 4 | Invoice Number | TRINVOICE | CHAR | 15
1429 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1430 6 | Transaction Detail | DETAILS | CHAR | 100
1431 7 | Amount | AMT | NUM* | 9
1432 8 | Line Format Control** | LNCTRL | CHAR | 2
1433 9 | Grouping Code | GROUP | CHAR | 2
1434 10 | User Defined | ACCT CODE | CHAR | 15
1439 my($self, %opt) = @_;
1441 eval "use Text::CSV_XS";
1444 my $cust_main = $self->cust_main;
1446 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1448 if ( lc($opt{'format'}) eq 'billco' ) {
1451 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1453 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1455 my( $previous_balance, @unused ) = $self->previous; #previous balance
1457 my $pmt_cr_applied = 0;
1458 $pmt_cr_applied += $_->{'amount'}
1459 foreach ( $self->_items_payments, $self->_items_credits ) ;
1461 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1464 '', # 1 | N/A-Leave Empty CHAR 2
1465 '', # 2 | N/A-Leave Empty CHAR 15
1466 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1467 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1468 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1469 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1470 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1471 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1472 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1473 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1474 '', # 10 | Ancillary Billing Information CHAR 30
1475 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1476 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1479 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1482 $duedate, # 14 | Bill Due Date CHAR 10
1484 $previous_balance, # 15 | Previous Balance NUM* 9
1485 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1486 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1487 $totaldue, # 18 | Total Amt Due NUM* 9
1488 $totaldue, # 19 | Total Amt Due NUM* 9
1489 '', # 20 | 30 Day Aging NUM* 9
1490 '', # 21 | 60 Day Aging NUM* 9
1491 '', # 22 | 90 Day Aging NUM* 9
1492 'N', # 23 | Y/N CHAR 1
1493 '', # 24 | Remittance automation CHAR 100
1494 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1495 $self->custnum, # 26 | Customer Reference Number CHAR 15
1496 '0', # 27 | Federal Tax*** NUM* 9
1497 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1498 '0', # 29 | Other Taxes & Fees*** NUM* 9
1507 time2str("%x", $self->_date),
1508 sprintf("%.2f", $self->charged),
1509 ( map { $cust_main->getfield($_) }
1510 qw( first last company address1 address2 city state zip country ) ),
1512 ) or die "can't create csv";
1515 my $header = $csv->string. "\n";
1518 if ( lc($opt{'format'}) eq 'billco' ) {
1521 foreach my $item ( $self->_items_pkg ) {
1524 '', # 1 | N/A-Leave Empty CHAR 2
1525 '', # 2 | N/A-Leave Empty CHAR 15
1526 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1527 $self->invnum, # 4 | Invoice Number CHAR 15
1528 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1529 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1530 $item->{'amount'}, # 7 | Amount NUM* 9
1531 '', # 8 | Line Format Control** CHAR 2
1532 '', # 9 | Grouping Code CHAR 2
1533 '', # 10 | User Defined CHAR 15
1536 $detail .= $csv->string. "\n";
1542 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1544 my($pkg, $setup, $recur, $sdate, $edate);
1545 if ( $cust_bill_pkg->pkgnum ) {
1547 ($pkg, $setup, $recur, $sdate, $edate) = (
1548 $cust_bill_pkg->part_pkg->pkg,
1549 ( $cust_bill_pkg->setup != 0
1550 ? sprintf("%.2f", $cust_bill_pkg->setup )
1552 ( $cust_bill_pkg->recur != 0
1553 ? sprintf("%.2f", $cust_bill_pkg->recur )
1555 ( $cust_bill_pkg->sdate
1556 ? time2str("%x", $cust_bill_pkg->sdate)
1558 ($cust_bill_pkg->edate
1559 ?time2str("%x", $cust_bill_pkg->edate)
1563 } else { #pkgnum tax
1564 next unless $cust_bill_pkg->setup != 0;
1565 $pkg = $cust_bill_pkg->desc;
1566 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1567 ( $sdate, $edate ) = ( '', '' );
1573 ( map { '' } (1..11) ),
1574 ($pkg, $setup, $recur, $sdate, $edate)
1575 ) or die "can't create csv";
1577 $detail .= $csv->string. "\n";
1583 ( $header, $detail );
1589 Pays this invoice with a compliemntary payment. If there is an error,
1590 returns the error, otherwise returns false.
1596 my $cust_pay = new FS::cust_pay ( {
1597 'invnum' => $self->invnum,
1598 'paid' => $self->owed,
1601 'payinfo' => $self->cust_main->payinfo,
1609 Attempts to pay this invoice with a credit card payment via a
1610 Business::OnlinePayment realtime gateway. See
1611 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1612 for supported processors.
1618 $self->realtime_bop( 'CC', @_ );
1623 Attempts to pay this invoice with an electronic check (ACH) payment via a
1624 Business::OnlinePayment realtime gateway. See
1625 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1626 for supported processors.
1632 $self->realtime_bop( 'ECHECK', @_ );
1637 Attempts to pay this invoice with phone bill (LEC) payment via a
1638 Business::OnlinePayment realtime gateway. See
1639 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1640 for supported processors.
1646 $self->realtime_bop( 'LEC', @_ );
1650 my( $self, $method ) = @_;
1652 my $cust_main = $self->cust_main;
1653 my $balance = $cust_main->balance;
1654 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1655 $amount = sprintf("%.2f", $amount);
1656 return "not run (balance $balance)" unless $amount > 0;
1658 my $description = 'Internet Services';
1659 if ( $conf->exists('business-onlinepayment-description') ) {
1660 my $dtempl = $conf->config('business-onlinepayment-description');
1662 my $agent_obj = $cust_main->agent
1663 or die "can't retreive agent for $cust_main (agentnum ".
1664 $cust_main->agentnum. ")";
1665 my $agent = $agent_obj->agent;
1666 my $pkgs = join(', ',
1667 map { $_->part_pkg->pkg }
1668 grep { $_->pkgnum } $self->cust_bill_pkg
1670 $description = eval qq("$dtempl");
1673 $cust_main->realtime_bop($method, $amount,
1674 'description' => $description,
1675 'invnum' => $self->invnum,
1680 =item batch_card OPTION => VALUE...
1682 Adds a payment for this invoice to the pending credit card batch (see
1683 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1684 runs the payment using a realtime gateway.
1689 my ($self, %options) = @_;
1690 my $cust_main = $self->cust_main;
1692 $options{invnum} = $self->invnum;
1694 $cust_main->batch_card(%options);
1697 sub _agent_template {
1699 $self->cust_main->agent_template;
1702 sub _agent_invoice_from {
1704 $self->cust_main->agent_invoice_from;
1707 =item print_text [ TIME [ , TEMPLATE ] ]
1709 Returns an text invoice, as a list of lines.
1711 TIME an optional value used to control the printing of overdue messages. The
1712 default is now. It isn't the date of the invoice; that's the `_date' field.
1713 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1714 L<Time::Local> and L<Date::Parse> for conversion functions.
1719 my( $self, $today, $template, %opt ) = @_;
1721 my %params = ( 'format' => 'template' );
1722 $params{'time'} = $today if $today;
1723 $params{'template'} = $template if $template;
1724 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1726 $self->print_generic( %params );
1729 =item print_latex [ TIME [ , TEMPLATE ] ]
1731 Internal method - returns a filename of a filled-in LaTeX template for this
1732 invoice (Note: add ".tex" to get the actual filename), and a filename of
1733 an associated logo (with the .eps extension included).
1735 See print_ps and print_pdf for methods that return PostScript and PDF output.
1737 TIME an optional value used to control the printing of overdue messages. The
1738 default is now. It isn't the date of the invoice; that's the `_date' field.
1739 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1740 L<Time::Local> and L<Date::Parse> for conversion functions.
1745 my( $self, $today, $template, %opt ) = @_;
1747 my %params = ( 'format' => 'latex' );
1748 $params{'time'} = $today if $today;
1749 $params{'template'} = $template if $template;
1750 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1752 $template ||= $self->_agent_template;
1754 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1755 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1759 ) or die "can't open temp file: $!\n";
1761 my $agentnum = $self->cust_main->agentnum;
1763 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1764 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1765 or die "can't write temp file: $!\n";
1767 print $lh $conf->config_binary('logo.eps', $agentnum)
1768 or die "can't write temp file: $!\n";
1771 $params{'logo_file'} = $lh->filename;
1773 my @filled_in = $self->print_generic( %params );
1775 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1779 ) or die "can't open temp file: $!\n";
1780 print $fh join('', @filled_in );
1783 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1784 return ($1, $params{'logo_file'});
1788 =item print_generic OPTIONS_HASH
1790 Internal method - returns a filled-in template for this invoice as a scalar.
1792 See print_ps and print_pdf for methods that return PostScript and PDF output.
1794 Non optional options include
1795 format - latex, html, template
1797 Optional options include
1799 template - a value used as a suffix for a configuration template
1801 time - a value used to control the printing of overdue messages. The
1802 default is now. It isn't the date of the invoice; that's the `_date' field.
1803 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1804 L<Time::Local> and L<Date::Parse> for conversion functions.
1808 unsquelch_cdr - overrides any per customer cdr squelching when true
1812 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1813 # (alignment?) problems to change them all to '%.2f' ?
1816 my( $self, %params ) = @_;
1817 my $today = $params{today} ? $params{today} : time;
1818 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1821 my $format = $params{format};
1822 die "Unknown format: $format"
1823 unless $format =~ /^(latex|html|template)$/;
1825 my $cust_main = $self->cust_main;
1826 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1827 unless $cust_main->payname
1828 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1830 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1831 'html' => [ '<%=', '%>' ],
1832 'template' => [ '{', '}' ],
1835 #create the template
1836 my $template = $params{template} ? $params{template} : $self->_agent_template;
1837 my $templatefile = "invoice_$format";
1838 $templatefile .= "_$template"
1839 if length($template);
1840 my @invoice_template = map "$_\n", $conf->config($templatefile)
1841 or die "cannot load config data $templatefile";
1844 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1845 #change this to a die when the old code is removed
1846 warn "old-style invoice template $templatefile; ".
1847 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1848 $old_latex = 'true';
1849 @invoice_template = _translate_old_latex_format(@invoice_template);
1852 my $text_template = new Text::Template(
1854 SOURCE => \@invoice_template,
1855 DELIMITERS => $delimiters{$format},
1858 $text_template->compile()
1859 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1862 # additional substitution could possibly cause breakage in existing templates
1863 my %convert_maps = (
1865 'notes' => sub { map "$_", @_ },
1866 'footer' => sub { map "$_", @_ },
1867 'smallfooter' => sub { map "$_", @_ },
1868 'returnaddress' => sub { map "$_", @_ },
1869 'coupon' => sub { map "$_", @_ },
1875 s/%%(.*)$/<!-- $1 -->/g;
1876 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1877 s/\\begin\{enumerate\}/<ol>/g;
1879 s/\\end\{enumerate\}/<\/ol>/g;
1880 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1889 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1891 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1896 s/\\\\\*?\s*$/<BR>/;
1897 s/\\hyphenation\{[\w\s\-]+}//;
1902 'coupon' => sub { "" },
1909 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1910 s/\\begin\{enumerate\}//g;
1912 s/\\end\{enumerate\}//g;
1913 s/\\textbf\{(.*)\}/$1/g;
1920 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1922 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1927 s/\\\\\*?\s*$/\n/; # dubious
1928 s/\\hyphenation\{[\w\s\-]+}//;
1932 'coupon' => sub { "" },
1937 # hashes for differing output formats
1938 my %nbsps = ( 'latex' => '~',
1939 'html' => '', # '&nbps;' would be nice
1940 'template' => '', # not used
1942 my $nbsp = $nbsps{$format};
1944 my %escape_functions = ( 'latex' => \&_latex_escape,
1945 'html' => \&encode_entities,
1946 'template' => sub { shift },
1948 my $escape_function = $escape_functions{$format};
1950 my %date_formats = ( 'latex' => '%b %o, %Y',
1951 'html' => '%b %o, %Y',
1954 my $date_format = $date_formats{$format};
1956 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1958 'html' => sub { return '<b>'. shift(). '</b>'
1960 'template' => sub { shift },
1962 my $embolden_function = $embolden_functions{$format};
1965 # generate template variables
1968 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1972 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1978 $returnaddress = join("\n",
1979 $conf->config_orbase("invoice_${format}returnaddress", $template)
1982 } elsif ( grep /\S/,
1983 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1985 my $convert_map = $convert_maps{$format}{'returnaddress'};
1988 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1993 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1995 my $convert_map = $convert_maps{$format}{'returnaddress'};
1996 $returnaddress = join( "\n", &$convert_map(
1997 map { s/( {2,})/'~' x length($1)/eg;
2001 ( $conf->config('company_name', $self->cust_main->agentnum),
2002 $conf->config('company_address', $self->cust_main->agentnum),
2009 my $warning = "Couldn't find a return address; ".
2010 "do you need to set the company_address configuration value?";
2012 $returnaddress = $nbsp;
2013 #$returnaddress = $warning;
2017 my %invoice_data = (
2018 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2019 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2020 'custnum' => $cust_main->display_custnum,
2021 'invnum' => $self->invnum,
2022 'date' => time2str($date_format, $self->_date),
2023 'today' => time2str('%b %o, %Y', $today),
2024 'agent' => &$escape_function($cust_main->agent->agent),
2025 'agent_custid' => &$escape_function($cust_main->agent_custid),
2026 'payname' => &$escape_function($cust_main->payname),
2027 'company' => &$escape_function($cust_main->company),
2028 'address1' => &$escape_function($cust_main->address1),
2029 'address2' => &$escape_function($cust_main->address2),
2030 'city' => &$escape_function($cust_main->city),
2031 'state' => &$escape_function($cust_main->state),
2032 'zip' => &$escape_function($cust_main->zip),
2033 'fax' => &$escape_function($cust_main->fax),
2034 'returnaddress' => $returnaddress,
2036 'terms' => $self->terms,
2037 'template' => $template, #params{'template'},
2038 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
2039 # better hang on to conf_dir for a while
2040 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2043 'current_charges' => sprintf("%.2f", $self->charged),
2044 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2045 'ship_enable' => $conf->exists('invoice-ship_address'),
2046 'unitprices' => $conf->exists('invoice-unitprice'),
2049 my $countrydefault = $conf->config('countrydefault') || 'US';
2050 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2051 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2052 my $method = $prefix.$_;
2053 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2055 $invoice_data{'ship_country'} = ''
2056 if ( $invoice_data{'ship_country'} eq $countrydefault );
2058 $invoice_data{'cid'} = $params{'cid'}
2061 if ( $cust_main->country eq $countrydefault ) {
2062 $invoice_data{'country'} = '';
2064 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2068 $invoice_data{'address'} = \@address;
2070 $cust_main->payname.
2071 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2072 ? " (P.O. #". $cust_main->payinfo. ")"
2076 push @address, $cust_main->company
2077 if $cust_main->company;
2078 push @address, $cust_main->address1;
2079 push @address, $cust_main->address2
2080 if $cust_main->address2;
2082 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2083 push @address, $invoice_data{'country'}
2084 if $invoice_data{'country'};
2086 while (scalar(@address) < 5);
2088 $invoice_data{'logo_file'} = $params{'logo_file'}
2089 if $params{'logo_file'};
2091 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2092 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2093 #my $balance_due = $self->owed + $pr_total - $cr_total;
2094 my $balance_due = $self->owed + $pr_total;
2095 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2096 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2098 my $agentnum = $self->cust_main->agentnum;
2100 #do variable substitution in notes, footer, smallfooter
2101 foreach my $include (qw( notes footer smallfooter coupon )) {
2103 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2106 if ( $conf->exists($inc_file, $agentnum)
2107 && length( $conf->config($inc_file, $agentnum) ) ) {
2109 @inc_src = $conf->config($inc_file, $agentnum);
2113 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2115 my $convert_map = $convert_maps{$format}{$include};
2117 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2118 s/--\@\]/$delimiters{$format}[1]/g;
2121 &$convert_map( $conf->config($inc_file, $agentnum) );
2125 my $inc_tt = new Text::Template (
2127 SOURCE => [ map "$_\n", @inc_src ],
2128 DELIMITERS => $delimiters{$format},
2129 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2131 unless ( $inc_tt->compile() ) {
2132 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2133 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2137 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2139 $invoice_data{$include} =~ s/\n+$//
2140 if ($format eq 'latex');
2143 $invoice_data{'po_line'} =
2144 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2145 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2148 my %money_chars = ( 'latex' => '',
2149 'html' => $conf->config('money_char') || '$',
2152 my $money_char = $money_chars{$format};
2154 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2155 'html' => $conf->config('money_char') || '$',
2158 my $other_money_char = $other_money_chars{$format};
2160 my @detail_items = ();
2161 my @total_items = ();
2165 $invoice_data{'detail_items'} = \@detail_items;
2166 $invoice_data{'total_items'} = \@total_items;
2167 $invoice_data{'buf'} = \@buf;
2168 $invoice_data{'sections'} = \@sections;
2170 my $previous_section = { 'description' => 'Previous Charges',
2171 'subtotal' => $other_money_char.
2172 sprintf('%.2f', $pr_total),
2176 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2177 'subtotal' => $taxtotal }; # adjusted below
2179 my $adjusttotal = 0;
2180 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2181 'subtotal' => 0 }; # adjusted below
2183 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2184 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2185 my $late_sections = [];
2186 if ( $multisection ) {
2187 push @sections, $self->_items_sections( $late_sections );
2189 push @sections, { 'description' => '', 'subtotal' => '' };
2192 unless ( $conf->exists('disable_previous_balance')
2193 || $conf->exists('previous_balance-summary_only')
2197 foreach my $line_item ( $self->_items_previous ) {
2200 ext_description => [],
2202 $detail->{'ref'} = $line_item->{'pkgnum'};
2203 $detail->{'quantity'} = 1;
2204 $detail->{'section'} = $previous_section;
2205 $detail->{'description'} = &$escape_function($line_item->{'description'});
2206 if ( exists $line_item->{'ext_description'} ) {
2207 @{$detail->{'ext_description'}} = map {
2208 &$escape_function($_);
2209 } @{$line_item->{'ext_description'}};
2211 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2212 $line_item->{'amount'};
2213 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2215 push @detail_items, $detail;
2216 push @buf, [ $detail->{'description'},
2217 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2223 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2224 push @buf, ['','-----------'];
2225 push @buf, [ 'Total Previous Balance',
2226 $money_char. sprintf("%10.2f", $pr_total) ];
2230 foreach my $section (@sections, @$late_sections) {
2232 $section->{'subtotal'} = $other_money_char.
2233 sprintf('%.2f', $section->{'subtotal'})
2236 if ( $section->{'description'} ) {
2237 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2243 $options{'section'} = $section if $multisection;
2244 $options{'format'} = $format;
2245 $options{'escape_function'} = $escape_function;
2246 $options{'format_function'} = sub { () } unless $unsquelched;
2247 $options{'unsquelched'} = $unsquelched;
2249 foreach my $line_item ( $self->_items_pkg(%options) ) {
2251 ext_description => [],
2253 $detail->{'ref'} = $line_item->{'pkgnum'};
2254 $detail->{'quantity'} = $line_item->{'quantity'};
2255 $detail->{'section'} = $section;
2256 $detail->{'description'} = &$escape_function($line_item->{'description'});
2257 if ( exists $line_item->{'ext_description'} ) {
2258 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2260 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2261 $line_item->{'amount'};
2262 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2263 $line_item->{'unit_amount'};
2264 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2266 push @detail_items, $detail;
2267 push @buf, ( [ $detail->{'description'},
2268 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2270 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2274 if ( $section->{'description'} ) {
2275 push @buf, ( ['','-----------'],
2276 [ $section->{'description'}. ' sub-total',
2277 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2286 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2287 unshift @sections, $previous_section if $pr_total;
2290 foreach my $tax ( $self->_items_tax ) {
2292 $taxtotal += $tax->{'amount'};
2294 my $description = &$escape_function( $tax->{'description'} );
2295 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2297 if ( $multisection ) {
2299 my $money = $old_latex ? '' : $money_char;
2300 push @detail_items, {
2301 ext_description => [],
2304 description => $description,
2305 amount => $money. $amount,
2307 section => $tax_section,
2312 push @total_items, {
2313 'total_item' => $description,
2314 'total_amount' => $other_money_char. $amount,
2319 push @buf,[ $description,
2320 $money_char. $amount,
2327 $total->{'total_item'} = 'Sub-total';
2328 $total->{'total_amount'} =
2329 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2331 if ( $multisection ) {
2332 $tax_section->{'subtotal'} = $other_money_char.
2333 sprintf('%.2f', $taxtotal);
2334 $tax_section->{'pretotal'} = 'New charges sub-total '.
2335 $total->{'total_amount'};
2336 push @sections, $tax_section if $taxtotal;
2338 unshift @total_items, $total;
2341 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2343 push @buf,['','-----------'];
2344 push @buf,[( $conf->exists('disable_previous_balance')
2346 : 'Total New Charges'
2348 $money_char. sprintf("%10.2f",$self->charged) ];
2353 $total->{'total_item'} = &$embolden_function('Total');
2354 $total->{'total_amount'} =
2355 &$embolden_function(
2358 $self->charged + ( $conf->exists('disable_previous_balance')
2364 if ( $multisection ) {
2365 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2366 sprintf('%.2f', $self->charged );
2368 push @total_items, $total;
2370 push @buf,['','-----------'];
2371 push @buf,['Total Charges',
2373 sprintf( '%10.2f', $self->charged +
2374 ( $conf->exists('disable_previous_balance')
2383 unless ( $conf->exists('disable_previous_balance') ) {
2384 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2387 my $credittotal = 0;
2388 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2391 $total->{'total_item'} = &$escape_function($credit->{'description'});
2392 $credittotal += $credit->{'amount'};
2393 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2394 $adjusttotal += $credit->{'amount'};
2395 if ( $multisection ) {
2396 my $money = $old_latex ? '' : $money_char;
2397 push @detail_items, {
2398 ext_description => [],
2401 description => &$escape_function($credit->{'description'}),
2402 amount => $money. $credit->{'amount'},
2404 section => $adjust_section,
2407 push @total_items, $total;
2411 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2414 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2415 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2419 my $paymenttotal = 0;
2420 foreach my $payment ( $self->_items_payments ) {
2422 $total->{'total_item'} = &$escape_function($payment->{'description'});
2423 $paymenttotal += $payment->{'amount'};
2424 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2425 $adjusttotal += $payment->{'amount'};
2426 if ( $multisection ) {
2427 my $money = $old_latex ? '' : $money_char;
2428 push @detail_items, {
2429 ext_description => [],
2432 description => &$escape_function($payment->{'description'}),
2433 amount => $money. $payment->{'amount'},
2435 section => $adjust_section,
2438 push @total_items, $total;
2440 push @buf, [ $payment->{'description'},
2441 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2444 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2446 if ( $multisection ) {
2447 $adjust_section->{'subtotal'} = $other_money_char.
2448 sprintf('%.2f', $adjusttotal);
2449 push @sections, $adjust_section;
2454 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2455 $total->{'total_amount'} =
2456 &$embolden_function(
2457 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2459 if ( $multisection ) {
2460 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2461 $total->{'total_amount'};
2463 push @total_items, $total;
2465 push @buf,['','-----------'];
2466 push @buf,[$self->balance_due_msg, $money_char.
2467 sprintf("%10.2f", $balance_due ) ];
2471 if ( $multisection ) {
2472 push @sections, @$late_sections
2478 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2479 /invoice_lines\((\d*)\)/;
2480 $invoice_lines += $1 || scalar(@buf);
2483 die "no invoice_lines() functions in template?"
2484 if ( $format eq 'template' && !$wasfunc );
2486 if ($format eq 'template') {
2488 if ( $invoice_lines ) {
2489 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2490 $invoice_data{'total_pages'}++
2491 if scalar(@buf) % $invoice_lines;
2494 #setup subroutine for the template
2495 sub FS::cust_bill::_template::invoice_lines {
2496 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2498 scalar(@FS::cust_bill::_template::buf)
2499 ? shift @FS::cust_bill::_template::buf
2508 push @collect, split("\n",
2509 $text_template->fill_in( HASH => \%invoice_data,
2510 PACKAGE => 'FS::cust_bill::_template'
2513 $FS::cust_bill::_template::page++;
2515 map "$_\n", @collect;
2517 warn "filling in template for invoice ". $self->invnum. "\n"
2519 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2522 $text_template->fill_in(HASH => \%invoice_data);
2526 =item print_ps [ TIME [ , TEMPLATE ] ]
2528 Returns an postscript invoice, as a scalar.
2530 TIME an optional value used to control the printing of overdue messages. The
2531 default is now. It isn't the date of the invoice; that's the `_date' field.
2532 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2533 L<Time::Local> and L<Date::Parse> for conversion functions.
2540 my ($file, $lfile) = $self->print_latex(@_);
2541 my $ps = generate_ps($file);
2547 =item print_pdf [ TIME [ , TEMPLATE ] ]
2549 Returns an PDF invoice, as a scalar.
2551 TIME an optional value used to control the printing of overdue messages. The
2552 default is now. It isn't the date of the invoice; that's the `_date' field.
2553 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2554 L<Time::Local> and L<Date::Parse> for conversion functions.
2561 my ($file, $lfile) = $self->print_latex(@_);
2562 my $pdf = generate_pdf($file);
2568 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2570 Returns an HTML 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.
2577 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2578 when emailing the invoice as part of a multipart/related MIME email.
2586 %params = %{ shift() };
2588 $params{'time'} = shift;
2589 $params{'template'} = shift;
2590 $params{'cid'} = shift;
2593 $params{'format'} = 'html';
2595 $self->print_generic( %params );
2598 # quick subroutine for print_latex
2600 # There are ten characters that LaTeX treats as special characters, which
2601 # means that they do not simply typeset themselves:
2602 # # $ % & ~ _ ^ \ { }
2604 # TeX ignores blanks following an escaped character; if you want a blank (as
2605 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2609 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2610 $value =~ s/([<>])/\$$1\$/g;
2614 #utility methods for print_*
2616 sub _translate_old_latex_format {
2617 warn "_translate_old_latex_format called\n"
2624 if ( $line =~ /^%%Detail\s*$/ ) {
2626 push @template, q![@--!,
2627 q! foreach my $_tr_line (@detail_items) {!,
2628 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2629 q! $_tr_line->{'description'} .= !,
2630 q! "\\tabularnewline\n~~".!,
2631 q! join( "\\tabularnewline\n~~",!,
2632 q! @{$_tr_line->{'ext_description'}}!,
2636 while ( ( my $line_item_line = shift )
2637 !~ /^%%EndDetail\s*$/ ) {
2638 $line_item_line =~ s/'/\\'/g; # nice LTS
2639 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2640 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2641 push @template, " \$OUT .= '$line_item_line';";
2644 push @template, '}',
2647 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2649 push @template, '[@--',
2650 ' foreach my $_tr_line (@total_items) {';
2652 while ( ( my $total_item_line = shift )
2653 !~ /^%%EndTotalDetails\s*$/ ) {
2654 $total_item_line =~ s/'/\\'/g; # nice LTS
2655 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2656 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2657 push @template, " \$OUT .= '$total_item_line';";
2660 push @template, '}',
2664 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2665 push @template, $line;
2671 warn "$_\n" foreach @template;
2680 #check for an invoice- specific override (eventually)
2682 #check for a customer- specific override
2683 return $self->cust_main->invoice_terms
2684 if $self->cust_main->invoice_terms;
2686 #use configured default
2687 $conf->config('invoice_default_terms') || '';
2693 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2694 $duedate = $self->_date() + ( $1 * 86400 );
2701 $self->due_date ? time2str(shift, $self->due_date) : '';
2704 sub balance_due_msg {
2706 my $msg = 'Balance Due';
2707 return $msg unless $self->terms;
2708 if ( $self->due_date ) {
2709 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2710 } elsif ( $self->terms ) {
2711 $msg .= ' - '. $self->terms;
2716 sub balance_due_date {
2719 if ( $conf->exists('invoice_default_terms')
2720 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2721 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2726 =item invnum_date_pretty
2728 Returns a string with the invoice number and date, for example:
2729 "Invoice #54 (3/20/2008)"
2733 sub invnum_date_pretty {
2735 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2740 Returns a string with the date, for example: "3/20/2008"
2746 time2str('%x', $self->_date);
2749 sub _items_sections {
2756 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2759 if ( $cust_bill_pkg->pkgnum > 0 ) {
2760 my $usage = $cust_bill_pkg->usage;
2762 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2763 my $desc = $display->section;
2764 my $type = $display->type;
2766 if ( $display->post_total ) {
2767 if (! $type || $type eq 'S') {
2768 $l{$desc} += $cust_bill_pkg->setup
2769 if ( $cust_bill_pkg->setup != 0 );
2773 $l{$desc} += $cust_bill_pkg->recur
2774 if ( $cust_bill_pkg->recur != 0 );
2777 if ($type && $type eq 'R') {
2778 $l{$desc} += $cust_bill_pkg->recur - $usage
2779 if ( $cust_bill_pkg->recur != 0 );
2782 if ($type && $type eq 'U') {
2783 $l{$desc} += $usage;
2787 if (! $type || $type eq 'S') {
2788 $s{$desc} += $cust_bill_pkg->setup
2789 if ( $cust_bill_pkg->setup != 0 );
2793 $s{$desc} += $cust_bill_pkg->recur
2794 if ( $cust_bill_pkg->recur != 0 );
2797 if ($type && $type eq 'R') {
2798 $s{$desc} += $cust_bill_pkg->recur - $usage
2799 if ( $cust_bill_pkg->recur != 0 );
2802 if ($type && $type eq 'U') {
2803 $s{$desc} += $usage;
2814 push @$late, map { { 'description' => $_,
2815 'subtotal' => $l{$_},
2819 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2826 #my @display = scalar(@_)
2828 # : qw( _items_previous _items_pkg );
2829 # #: qw( _items_pkg );
2830 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2831 my @display = qw( _items_previous _items_pkg );
2834 foreach my $display ( @display ) {
2835 push @b, $self->$display(@_);
2840 sub _items_previous {
2842 my $cust_main = $self->cust_main;
2843 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2845 foreach ( @pr_cust_bill ) {
2847 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2848 ' ('. time2str('%x',$_->_date). ')',
2849 #'pkgpart' => 'N/A',
2851 'amount' => sprintf("%.2f", $_->owed),
2857 # 'description' => 'Previous Balance',
2858 # #'pkgpart' => 'N/A',
2859 # 'pkgnum' => 'N/A',
2860 # 'amount' => sprintf("%10.2f", $pr_total ),
2861 # 'ext_description' => [ map {
2862 # "Invoice ". $_->invnum.
2863 # " (". time2str("%x",$_->_date). ") ".
2864 # sprintf("%10.2f", $_->owed)
2865 # } @pr_cust_bill ],
2872 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2873 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2877 return 0 unless $a cmp $b;
2878 return -1 if $b eq 'Tax';
2879 return 1 if $a eq 'Tax';
2880 return -1 if $b eq 'Other surcharges';
2881 return 1 if $a eq 'Other surcharges';
2887 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2888 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2891 sub _items_cust_bill_pkg {
2893 my $cust_bill_pkg = shift;
2896 my $format = $opt{format} || '';
2897 my $escape_function = $opt{escape_function} || sub { shift };
2898 my $format_function = $opt{format_function} || '';
2899 my $unsquelched = $opt{unsquelched} || '';
2900 my $section = $opt{section}->{description} if $opt{section};
2903 my ($s, $r, $u) = ( undef, undef, undef );
2904 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2907 foreach ( $s, $r, $u ) {
2908 if ( $_ && !$cust_bill_pkg->hidden ) {
2909 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2910 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2916 foreach my $display ( grep { defined($section)
2917 ? $_->section eq $section
2920 $cust_bill_pkg->cust_bill_pkg_display
2924 my $type = $display->type;
2926 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2928 my $desc = $cust_bill_pkg->desc;
2929 $desc = substr($desc, 0, 50). '...'
2930 if $format eq 'latex' && length($desc) > 50;
2932 my %details_opt = ( 'format' => $format,
2933 'escape_function' => $escape_function,
2934 'format_function' => $format_function,
2937 if ( $cust_bill_pkg->pkgnum > 0 ) {
2939 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2941 my $description = $desc;
2942 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2945 push @d, map &{$escape_function}($_),
2946 $cust_pkg->h_labels_short($self->_date)
2947 unless $cust_pkg->part_pkg->hide_svc_detail
2948 || $cust_bill_pkg->hidden;
2949 push @d, $cust_bill_pkg->details(%details_opt)
2950 if $cust_bill_pkg->recur == 0;
2952 if ( $cust_bill_pkg->hidden ) {
2953 $s->{amount} += $cust_bill_pkg->setup;
2954 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2955 push @{ $s->{ext_description} }, @d;
2958 description => $description,
2959 #pkgpart => $part_pkg->pkgpart,
2960 pkgnum => $cust_bill_pkg->pkgnum,
2961 amount => $cust_bill_pkg->setup,
2962 unit_amount => $cust_bill_pkg->unitsetup,
2963 quantity => $cust_bill_pkg->quantity,
2964 ext_description => \@d,
2970 if ( $cust_bill_pkg->recur != 0 &&
2971 ( !$type || $type eq 'R' || $type eq 'U' )
2975 my $is_summary = $display->summary;
2976 my $description = $is_summary ? "Usage charges" : $desc;
2978 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2979 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2980 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2985 #at least until cust_bill_pkg has "past" ranges in addition to
2986 #the "future" sdate/edate ones... see #3032
2987 my @dates = ( $self->_date );
2988 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2989 push @dates, $prev->sdate if $prev;
2991 push @d, map &{$escape_function}($_),
2992 $cust_pkg->h_labels_short(@dates)
2993 #$cust_bill_pkg->edate,
2994 #$cust_bill_pkg->sdate)
2995 unless $cust_pkg->part_pkg->hide_svc_detail
2996 || $cust_bill_pkg->itemdesc
2997 || $cust_bill_pkg->hidden
3000 push @d, $cust_bill_pkg->details(%details_opt)
3001 unless ($is_summary || $type && $type eq 'R');
3005 $amount = $cust_bill_pkg->recur;
3006 }elsif($type eq 'R') {
3007 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3008 }elsif($type eq 'U') {
3009 $amount = $cust_bill_pkg->usage;
3012 if ( !$type || $type eq 'R' ) {
3014 if ( $cust_bill_pkg->hidden ) {
3015 $r->{amount} += $amount;
3016 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3017 push @{ $r->{ext_description} }, @d;
3020 description => $description,
3021 #pkgpart => $part_pkg->pkgpart,
3022 pkgnum => $cust_bill_pkg->pkgnum,
3024 unit_amount => $cust_bill_pkg->unitrecur,
3025 quantity => $cust_bill_pkg->quantity,
3026 ext_description => \@d,
3030 } elsif ( $amount ) { # && $type eq 'U'
3032 if ( $cust_bill_pkg->hidden ) {
3033 $u->{amount} += $amount;
3034 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3035 push @{ $u->{ext_description} }, @d;
3038 description => $description,
3039 #pkgpart => $part_pkg->pkgpart,
3040 pkgnum => $cust_bill_pkg->pkgnum,
3042 unit_amount => $cust_bill_pkg->unitrecur,
3043 quantity => $cust_bill_pkg->quantity,
3044 ext_description => \@d,
3050 } # recurring or usage with recurring charge
3052 } else { #pkgnum tax or one-shot line item (??)
3054 if ( $cust_bill_pkg->setup != 0 ) {
3056 'description' => $desc,
3057 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3060 if ( $cust_bill_pkg->recur != 0 ) {
3062 'description' => "$desc (".
3063 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3064 time2str("%x", $cust_bill_pkg->edate). ')',
3065 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3075 foreach ( $s, $r, $u ) {
3077 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3078 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3087 sub _items_credits {
3088 my( $self, %opt ) = @_;
3089 my $trim_len = $opt{'trim_len'} || 60;
3093 foreach ( $self->cust_credited ) {
3095 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3097 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3098 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3099 $reason = " ($reason) " if $reason;
3102 #'description' => 'Credit ref\#'. $_->crednum.
3103 # " (". time2str("%x",$_->cust_credit->_date) .")".
3105 'description' => 'Credit applied '.
3106 time2str("%x",$_->cust_credit->_date). $reason,
3107 'amount' => sprintf("%.2f",$_->amount),
3115 sub _items_payments {
3119 #get & print payments
3120 foreach ( $self->cust_bill_pay ) {
3122 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3125 'description' => "Payment received ".
3126 time2str("%x",$_->cust_pay->_date ),
3127 'amount' => sprintf("%.2f", $_->amount )
3135 =item call_details [ OPTION => VALUE ... ]
3137 Returns an array of CSV strings representing the call details for this invoice
3138 The only option available is the boolean prepend_billed_number
3143 my ($self, %opt) = @_;
3145 my $format_function = sub { shift };
3147 if ($opt{prepend_billed_number}) {
3148 $format_function = sub {
3152 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3157 my @details = map { $_->details( 'format_function' => $format_function,
3158 'escape_function' => sub{ return() },
3162 $self->cust_bill_pkg;
3163 my $header = $details[0];
3164 ( $header, grep { $_ ne $header } @details );
3174 =item process_reprint
3178 sub process_reprint {
3179 process_re_X('print', @_);
3182 =item process_reemail
3186 sub process_reemail {
3187 process_re_X('email', @_);
3195 process_re_X('fax', @_);
3203 process_re_X('ftp', @_);
3210 sub process_respool {
3211 process_re_X('spool', @_);
3214 use Storable qw(thaw);
3218 my( $method, $job ) = ( shift, shift );
3219 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3221 my $param = thaw(decode_base64(shift));
3222 warn Dumper($param) if $DEBUG;
3233 my($method, $job, %param ) = @_;
3235 warn "re_X $method for job $job with param:\n".
3236 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3239 #some false laziness w/search/cust_bill.html
3241 my $orderby = 'ORDER BY cust_bill._date';
3243 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3245 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3247 my @cust_bill = qsearch( {
3248 #'select' => "cust_bill.*",
3249 'table' => 'cust_bill',
3250 'addl_from' => $addl_from,
3252 'extra_sql' => $extra_sql,
3253 'order_by' => $orderby,
3257 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3259 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3262 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3263 foreach my $cust_bill ( @cust_bill ) {
3264 $cust_bill->$method();
3266 if ( $job ) { #progressbar foo
3268 if ( time - $min_sec > $last ) {
3269 my $error = $job->update_statustext(
3270 int( 100 * $num / scalar(@cust_bill) )
3272 die $error if $error;
3283 =head1 CLASS METHODS
3289 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3295 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3300 Returns an SQL fragment to retreive the net amount (charged minus credited).
3306 'charged - '. $class->credited_sql;
3311 Returns an SQL fragment to retreive the amount paid against this invoice.
3317 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3318 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3323 Returns an SQL fragment to retreive the amount credited against this invoice.
3329 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3330 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3333 =item search_sql HASHREF
3335 Class method which returns an SQL WHERE fragment to search for parameters
3336 specified in HASHREF. Valid parameters are
3342 Epoch date (UNIX timestamp) setting a lower bound for _date values
3346 Epoch date (UNIX timestamp) setting an upper bound for _date values
3360 =item newest_percust
3364 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3369 my($class, $param) = @_;
3371 warn "$me search_sql called with params: \n".
3372 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3377 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3378 push @search, "cust_bill._date >= $1";
3380 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3381 push @search, "cust_bill._date < $1";
3383 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3384 push @search, "cust_bill.invnum >= $1";
3386 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3387 push @search, "cust_bill.invnum <= $1";
3389 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3390 push @search, "cust_main.agentnum = $1";
3393 push @search, '0 != '. FS::cust_bill->owed_sql
3394 if $param->{'open'};
3396 push @search, '0 != '. FS::cust_bill->net_sql
3399 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3400 if $param->{'days'};
3402 if ( $param->{'newest_percust'} ) {
3404 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3405 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3407 my @newest_where = map { my $x = $_;
3408 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3411 grep ! /^cust_main./, @search;
3412 my $newest_where = scalar(@newest_where)
3413 ? ' AND '. join(' AND ', @newest_where)
3417 push @search, "cust_bill._date = (
3418 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3419 WHERE newest_cust_bill.custnum = cust_bill.custnum
3425 my $curuser = $FS::CurrentUser::CurrentUser;
3426 if ( $curuser->username eq 'fs_queue'
3427 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3429 my $newuser = qsearchs('access_user', {
3430 'username' => $username,
3434 $curuser = $newuser;
3436 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3440 push @search, $curuser->agentnums_sql;
3442 join(' AND ', @search );
3454 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3455 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base