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
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
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" } $self->call_details ],
798 'Disposition' => 'attachment',
803 if ( $conf->exists('invoice_email_pdf') ) {
808 # multipart/alternative
814 my $related = build MIME::Entity 'Type' => 'multipart/related',
815 'Encoding' => '7bit';
817 #false laziness w/Misc::send_email
818 $related->head->replace('Content-type',
820 '; boundary="'. $related->head->multipart_boundary. '"'.
821 '; type=multipart/alternative'
824 $related->add_part($alternative);
826 $related->add_part($image);
828 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
830 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
834 #no other attachment:
836 # multipart/alternative
841 $return{'content-type'} = 'multipart/related';
842 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
843 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
844 #$return{'disposition'} = 'inline';
850 if ( $conf->exists('invoice_email_pdf') ) {
851 warn "$me creating PDF attachment"
854 #mime parts arguments a la MIME::Entity->build().
855 $return{'mimeparts'} = [
856 { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
860 if ( $conf->exists('invoice_email_pdf')
861 and scalar($conf->config('invoice_email_pdf_note')) ) {
863 warn "$me using 'invoice_email_pdf_note'"
865 $return{'body'} = [ map { $_ . "\n" }
866 $conf->config('invoice_email_pdf_note')
871 warn "$me not using 'invoice_email_pdf_note'"
873 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
874 $return{'body'} = $args{'print_text'};
876 $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
889 Returns a list suitable for passing to MIME::Entity->build(), representing
890 this invoice as PDF attachment.
897 'Type' => 'application/pdf',
898 'Encoding' => 'base64',
899 'Data' => [ $self->print_pdf(@_) ],
900 'Disposition' => 'attachment',
901 'Filename' => 'invoice-'. $self->invnum. '.pdf',
905 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
907 Sends this invoice to the destinations configured for this customer: sends
908 email, prints and/or faxes. See L<FS::cust_main_invoice>.
910 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
912 AGENTNUM, if specified, means that this invoice will only be sent for customers
913 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
914 single agent) or an arrayref of agentnums.
916 INVOICE_FROM, if specified, overrides the default email invoice From: address.
918 AMOUNT, if specified, only sends the invoice if the total amount owed on this
919 invoice and all older invoices is greater than the specified amount.
926 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
927 or die "invalid invoice number: " . $opt{invnum};
929 my @args = ( $opt{template}, $opt{agentnum} );
930 push @args, $opt{invoice_from}
931 if exists($opt{invoice_from}) && $opt{invoice_from};
933 my $error = $self->send( @args );
934 die $error if $error;
940 my $template = scalar(@_) ? shift : '';
941 if ( scalar(@_) && $_[0] ) {
942 my $agentnums = ref($_[0]) ? shift : [ shift ];
943 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
949 : ( $self->_agent_invoice_from || #XXX should go away
950 $conf->config('invoice_from', $self->cust_main->agentnum )
953 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
956 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
958 my @invoicing_list = $self->cust_main->invoicing_list;
960 #$self->email_invoice($template, $invoice_from)
961 $self->email($template, $invoice_from)
962 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
964 #$self->print_invoice($template)
965 $self->print($template)
966 if grep { $_ eq 'POST' } @invoicing_list; #postal
968 $self->fax_invoice($template)
969 if grep { $_ eq 'FAX' } @invoicing_list; #fax
975 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
979 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
981 INVOICE_FROM, if specified, overrides the default email invoice From: address.
985 sub queueable_email {
988 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
989 or die "invalid invoice number: " . $opt{invnum};
991 my @args = ( $opt{template} );
992 push @args, $opt{invoice_from}
993 if exists($opt{invoice_from}) && $opt{invoice_from};
995 my $error = $self->email( @args );
996 die $error if $error;
1000 #sub email_invoice {
1003 my $template = scalar(@_) ? shift : '';
1007 : ( $self->_agent_invoice_from || #XXX should go away
1008 $conf->config('invoice_from', $self->cust_main->agentnum )
1012 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1013 $self->cust_main->invoicing_list;
1015 #better to notify this person than silence
1016 @invoicing_list = ($invoice_from) unless @invoicing_list;
1018 my $subject = $self->email_subject($template);
1020 my $error = send_email(
1021 $self->generate_email(
1022 'from' => $invoice_from,
1023 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1024 'subject' => $subject,
1025 'template' => $template,
1028 die "can't email invoice: $error\n" if $error;
1029 #die "$error\n" if $error;
1036 #my $template = scalar(@_) ? shift : '';
1039 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1042 my $cust_main = $self->cust_main;
1043 my $name = $cust_main->name;
1044 my $name_short = $cust_main->name_short;
1045 my $invoice_number = $self->invnum;
1046 my $invoice_date = $self->_date_pretty;
1048 eval qq("$subject");
1051 =item lpr_data [ TEMPLATENAME ]
1053 Returns the postscript or plaintext for this invoice as an arrayref.
1055 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1060 my( $self, $template) = @_;
1061 $conf->exists('invoice_latex')
1062 ? [ $self->print_ps('', $template) ]
1063 : [ $self->print_text('', $template) ];
1066 =item print [ TEMPLATENAME ]
1068 Prints this invoice.
1070 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1074 #sub print_invoice {
1077 my $template = scalar(@_) ? shift : '';
1079 do_print $self->lpr_data($template);
1082 =item fax_invoice [ TEMPLATENAME ]
1086 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1092 my $template = scalar(@_) ? shift : '';
1094 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1095 unless $conf->exists('invoice_latex');
1097 my $dialstring = $self->cust_main->getfield('fax');
1100 my $error = send_fax( 'docdata' => $self->lpr_data($template),
1101 'dialstring' => $dialstring,
1103 die $error if $error;
1107 =item ftp_invoice [ TEMPLATENAME ]
1109 Sends this invoice data via FTP.
1111 TEMPLATENAME is unused?
1117 my $template = scalar(@_) ? shift : '';
1120 'protocol' => 'ftp',
1121 'server' => $conf->config('cust_bill-ftpserver'),
1122 'username' => $conf->config('cust_bill-ftpusername'),
1123 'password' => $conf->config('cust_bill-ftppassword'),
1124 'dir' => $conf->config('cust_bill-ftpdir'),
1125 'format' => $conf->config('cust_bill-ftpformat'),
1129 =item spool_invoice [ TEMPLATENAME ]
1131 Spools this invoice data (see L<FS::spool_csv>)
1133 TEMPLATENAME is unused?
1139 my $template = scalar(@_) ? shift : '';
1142 'format' => $conf->config('cust_bill-spoolformat'),
1143 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1147 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1149 Like B<send>, but only sends the invoice if it is the newest open invoice for
1154 sub send_if_newest {
1159 grep { $_->owed > 0 }
1160 qsearch('cust_bill', {
1161 'custnum' => $self->custnum,
1162 #'_date' => { op=>'>', value=>$self->_date },
1163 'invnum' => { op=>'>', value=>$self->invnum },
1170 =item send_csv OPTION => VALUE, ...
1172 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1176 protocol - currently only "ftp"
1182 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1183 and YYMMDDHHMMSS is a timestamp.
1185 See L</print_csv> for a description of the output format.
1190 my($self, %opt) = @_;
1194 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1195 mkdir $spooldir, 0700 unless -d $spooldir;
1197 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1198 my $file = "$spooldir/$tracctnum.csv";
1200 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1202 open(CSV, ">$file") or die "can't open $file: $!";
1210 if ( $opt{protocol} eq 'ftp' ) {
1211 eval "use Net::FTP;";
1213 $net = Net::FTP->new($opt{server}) or die @$;
1215 die "unknown protocol: $opt{protocol}";
1218 $net->login( $opt{username}, $opt{password} )
1219 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1221 $net->binary or die "can't set binary mode";
1223 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1225 $net->put($file) or die "can't put $file: $!";
1235 Spools CSV invoice data.
1241 =item format - 'default' or 'billco'
1243 =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>).
1245 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1247 =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.
1254 my($self, %opt) = @_;
1256 my $cust_main = $self->cust_main;
1258 if ( $opt{'dest'} ) {
1259 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1260 $cust_main->invoicing_list;
1261 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1262 || ! keys %invoicing_list;
1265 if ( $opt{'balanceover'} ) {
1267 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1270 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1271 mkdir $spooldir, 0700 unless -d $spooldir;
1273 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1277 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1278 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1281 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1283 open(CSV, ">>$file") or die "can't open $file: $!";
1284 flock(CSV, LOCK_EX);
1289 if ( lc($opt{'format'}) eq 'billco' ) {
1291 flock(CSV, LOCK_UN);
1296 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1299 open(CSV,">>$file") or die "can't open $file: $!";
1300 flock(CSV, LOCK_EX);
1306 flock(CSV, LOCK_UN);
1313 =item print_csv OPTION => VALUE, ...
1315 Returns CSV data for this invoice.
1319 format - 'default' or 'billco'
1321 Returns a list consisting of two scalars. The first is a single line of CSV
1322 header information for this invoice. The second is one or more lines of CSV
1323 detail information for this invoice.
1325 If I<format> is not specified or "default", the fields of the CSV file are as
1328 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1332 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1334 B<record_type> is C<cust_bill> for the initial header line only. The
1335 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1336 fields are filled in.
1338 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1339 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1342 =item invnum - invoice number
1344 =item custnum - customer number
1346 =item _date - invoice date
1348 =item charged - total invoice amount
1350 =item first - customer first name
1352 =item last - customer first name
1354 =item company - company name
1356 =item address1 - address line 1
1358 =item address2 - address line 1
1368 =item pkg - line item description
1370 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1372 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1374 =item sdate - start date for recurring fee
1376 =item edate - end date for recurring fee
1380 If I<format> is "billco", the fields of the header CSV file are as follows:
1382 +-------------------------------------------------------------------+
1383 | FORMAT HEADER FILE |
1384 |-------------------------------------------------------------------|
1385 | Field | Description | Name | Type | Width |
1386 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1387 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1388 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1389 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1390 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1391 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1392 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1393 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1394 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1395 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1396 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1397 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1398 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1399 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1400 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1401 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1402 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1403 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1404 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1405 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1406 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1407 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1408 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1409 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1410 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1411 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1412 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1413 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1414 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1415 +-------+-------------------------------+------------+------+-------+
1417 If I<format> is "billco", the fields of the detail CSV file are as follows:
1419 FORMAT FOR DETAIL FILE
1421 Field | Description | Name | Type | Width
1422 1 | N/A-Leave Empty | RC | CHAR | 2
1423 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1424 3 | Account Number | TRACCTNUM | CHAR | 15
1425 4 | Invoice Number | TRINVOICE | CHAR | 15
1426 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1427 6 | Transaction Detail | DETAILS | CHAR | 100
1428 7 | Amount | AMT | NUM* | 9
1429 8 | Line Format Control** | LNCTRL | CHAR | 2
1430 9 | Grouping Code | GROUP | CHAR | 2
1431 10 | User Defined | ACCT CODE | CHAR | 15
1436 my($self, %opt) = @_;
1438 eval "use Text::CSV_XS";
1441 my $cust_main = $self->cust_main;
1443 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1445 if ( lc($opt{'format'}) eq 'billco' ) {
1448 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1450 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1452 my( $previous_balance, @unused ) = $self->previous; #previous balance
1454 my $pmt_cr_applied = 0;
1455 $pmt_cr_applied += $_->{'amount'}
1456 foreach ( $self->_items_payments, $self->_items_credits ) ;
1458 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1461 '', # 1 | N/A-Leave Empty CHAR 2
1462 '', # 2 | N/A-Leave Empty CHAR 15
1463 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1464 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1465 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1466 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1467 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1468 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1469 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1470 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1471 '', # 10 | Ancillary Billing Information CHAR 30
1472 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1473 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1476 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1479 $duedate, # 14 | Bill Due Date CHAR 10
1481 $previous_balance, # 15 | Previous Balance NUM* 9
1482 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1483 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1484 $totaldue, # 18 | Total Amt Due NUM* 9
1485 $totaldue, # 19 | Total Amt Due NUM* 9
1486 '', # 20 | 30 Day Aging NUM* 9
1487 '', # 21 | 60 Day Aging NUM* 9
1488 '', # 22 | 90 Day Aging NUM* 9
1489 'N', # 23 | Y/N CHAR 1
1490 '', # 24 | Remittance automation CHAR 100
1491 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1492 $self->custnum, # 26 | Customer Reference Number CHAR 15
1493 '0', # 27 | Federal Tax*** NUM* 9
1494 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1495 '0', # 29 | Other Taxes & Fees*** NUM* 9
1504 time2str("%x", $self->_date),
1505 sprintf("%.2f", $self->charged),
1506 ( map { $cust_main->getfield($_) }
1507 qw( first last company address1 address2 city state zip country ) ),
1509 ) or die "can't create csv";
1512 my $header = $csv->string. "\n";
1515 if ( lc($opt{'format'}) eq 'billco' ) {
1518 foreach my $item ( $self->_items_pkg ) {
1521 '', # 1 | N/A-Leave Empty CHAR 2
1522 '', # 2 | N/A-Leave Empty CHAR 15
1523 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1524 $self->invnum, # 4 | Invoice Number CHAR 15
1525 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1526 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1527 $item->{'amount'}, # 7 | Amount NUM* 9
1528 '', # 8 | Line Format Control** CHAR 2
1529 '', # 9 | Grouping Code CHAR 2
1530 '', # 10 | User Defined CHAR 15
1533 $detail .= $csv->string. "\n";
1539 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1541 my($pkg, $setup, $recur, $sdate, $edate);
1542 if ( $cust_bill_pkg->pkgnum ) {
1544 ($pkg, $setup, $recur, $sdate, $edate) = (
1545 $cust_bill_pkg->part_pkg->pkg,
1546 ( $cust_bill_pkg->setup != 0
1547 ? sprintf("%.2f", $cust_bill_pkg->setup )
1549 ( $cust_bill_pkg->recur != 0
1550 ? sprintf("%.2f", $cust_bill_pkg->recur )
1552 ( $cust_bill_pkg->sdate
1553 ? time2str("%x", $cust_bill_pkg->sdate)
1555 ($cust_bill_pkg->edate
1556 ?time2str("%x", $cust_bill_pkg->edate)
1560 } else { #pkgnum tax
1561 next unless $cust_bill_pkg->setup != 0;
1562 $pkg = $cust_bill_pkg->desc;
1563 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1564 ( $sdate, $edate ) = ( '', '' );
1570 ( map { '' } (1..11) ),
1571 ($pkg, $setup, $recur, $sdate, $edate)
1572 ) or die "can't create csv";
1574 $detail .= $csv->string. "\n";
1580 ( $header, $detail );
1586 Pays this invoice with a compliemntary payment. If there is an error,
1587 returns the error, otherwise returns false.
1593 my $cust_pay = new FS::cust_pay ( {
1594 'invnum' => $self->invnum,
1595 'paid' => $self->owed,
1598 'payinfo' => $self->cust_main->payinfo,
1606 Attempts to pay this invoice with a credit card payment via a
1607 Business::OnlinePayment realtime gateway. See
1608 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1609 for supported processors.
1615 $self->realtime_bop( 'CC', @_ );
1620 Attempts to pay this invoice with an electronic check (ACH) payment via a
1621 Business::OnlinePayment realtime gateway. See
1622 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1623 for supported processors.
1629 $self->realtime_bop( 'ECHECK', @_ );
1634 Attempts to pay this invoice with phone bill (LEC) payment via a
1635 Business::OnlinePayment realtime gateway. See
1636 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1637 for supported processors.
1643 $self->realtime_bop( 'LEC', @_ );
1647 my( $self, $method ) = @_;
1649 my $cust_main = $self->cust_main;
1650 my $balance = $cust_main->balance;
1651 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1652 $amount = sprintf("%.2f", $amount);
1653 return "not run (balance $balance)" unless $amount > 0;
1655 my $description = 'Internet Services';
1656 if ( $conf->exists('business-onlinepayment-description') ) {
1657 my $dtempl = $conf->config('business-onlinepayment-description');
1659 my $agent_obj = $cust_main->agent
1660 or die "can't retreive agent for $cust_main (agentnum ".
1661 $cust_main->agentnum. ")";
1662 my $agent = $agent_obj->agent;
1663 my $pkgs = join(', ',
1664 map { $_->part_pkg->pkg }
1665 grep { $_->pkgnum } $self->cust_bill_pkg
1667 $description = eval qq("$dtempl");
1670 $cust_main->realtime_bop($method, $amount,
1671 'description' => $description,
1672 'invnum' => $self->invnum,
1677 =item batch_card OPTION => VALUE...
1679 Adds a payment for this invoice to the pending credit card batch (see
1680 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1681 runs the payment using a realtime gateway.
1686 my ($self, %options) = @_;
1687 my $cust_main = $self->cust_main;
1689 $options{invnum} = $self->invnum;
1691 $cust_main->batch_card(%options);
1694 sub _agent_template {
1696 $self->cust_main->agent_template;
1699 sub _agent_invoice_from {
1701 $self->cust_main->agent_invoice_from;
1704 =item print_text [ TIME [ , TEMPLATE ] ]
1706 Returns an text invoice, as a list of lines.
1708 TIME an optional value used to control the printing of overdue messages. The
1709 default is now. It isn't the date of the invoice; that's the `_date' field.
1710 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1711 L<Time::Local> and L<Date::Parse> for conversion functions.
1716 my( $self, $today, $template, %opt ) = @_;
1718 my %params = ( 'format' => 'template' );
1719 $params{'time'} = $today if $today;
1720 $params{'template'} = $template if $template;
1721 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1723 $self->print_generic( %params );
1726 =item print_latex [ TIME [ , TEMPLATE ] ]
1728 Internal method - returns a filename of a filled-in LaTeX template for this
1729 invoice (Note: add ".tex" to get the actual filename), and a filename of
1730 an associated logo (with the .eps extension included).
1732 See print_ps and print_pdf for methods that return PostScript and PDF output.
1734 TIME an optional value used to control the printing of overdue messages. The
1735 default is now. It isn't the date of the invoice; that's the `_date' field.
1736 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1737 L<Time::Local> and L<Date::Parse> for conversion functions.
1742 my( $self, $today, $template, %opt ) = @_;
1744 my %params = ( 'format' => 'latex' );
1745 $params{'time'} = $today if $today;
1746 $params{'template'} = $template if $template;
1747 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1749 $template ||= $self->_agent_template;
1751 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1752 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1756 ) or die "can't open temp file: $!\n";
1758 my $agentnum = $self->cust_main->agentnum;
1760 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1761 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1762 or die "can't write temp file: $!\n";
1764 print $lh $conf->config_binary('logo.eps', $agentnum)
1765 or die "can't write temp file: $!\n";
1768 $params{'logo_file'} = $lh->filename;
1770 my @filled_in = $self->print_generic( %params );
1772 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1776 ) or die "can't open temp file: $!\n";
1777 print $fh join('', @filled_in );
1780 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1781 return ($1, $params{'logo_file'});
1785 =item print_generic OPTIONS_HASH
1787 Internal method - returns a filled-in template for this invoice as a scalar.
1789 See print_ps and print_pdf for methods that return PostScript and PDF output.
1791 Non optional options include
1792 format - latex, html, template
1794 Optional options include
1796 template - a value used as a suffix for a configuration template
1798 time - a value used to control the printing of overdue messages. The
1799 default is now. It isn't the date of the invoice; that's the `_date' field.
1800 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1801 L<Time::Local> and L<Date::Parse> for conversion functions.
1805 unsquelch_cdr - overrides any per customer cdr squelching when true
1809 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1810 # (alignment?) problems to change them all to '%.2f' ?
1813 my( $self, %params ) = @_;
1814 my $today = $params{today} ? $params{today} : time;
1815 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1818 my $format = $params{format};
1819 die "Unknown format: $format"
1820 unless $format =~ /^(latex|html|template)$/;
1822 my $cust_main = $self->cust_main;
1823 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1824 unless $cust_main->payname
1825 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1827 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1828 'html' => [ '<%=', '%>' ],
1829 'template' => [ '{', '}' ],
1832 #create the template
1833 my $template = $params{template} ? $params{template} : $self->_agent_template;
1834 my $templatefile = "invoice_$format";
1835 $templatefile .= "_$template"
1836 if length($template);
1837 my @invoice_template = map "$_\n", $conf->config($templatefile)
1838 or die "cannot load config data $templatefile";
1841 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1842 #change this to a die when the old code is removed
1843 warn "old-style invoice template $templatefile; ".
1844 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1845 $old_latex = 'true';
1846 @invoice_template = _translate_old_latex_format(@invoice_template);
1849 my $text_template = new Text::Template(
1851 SOURCE => \@invoice_template,
1852 DELIMITERS => $delimiters{$format},
1855 $text_template->compile()
1856 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1859 # additional substitution could possibly cause breakage in existing templates
1860 my %convert_maps = (
1862 'notes' => sub { map "$_", @_ },
1863 'footer' => sub { map "$_", @_ },
1864 'smallfooter' => sub { map "$_", @_ },
1865 'returnaddress' => sub { map "$_", @_ },
1866 'coupon' => sub { map "$_", @_ },
1872 s/%%(.*)$/<!-- $1 -->/g;
1873 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1874 s/\\begin\{enumerate\}/<ol>/g;
1876 s/\\end\{enumerate\}/<\/ol>/g;
1877 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1886 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1888 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1893 s/\\\\\*?\s*$/<BR>/;
1894 s/\\hyphenation\{[\w\s\-]+}//;
1899 'coupon' => sub { "" },
1906 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1907 s/\\begin\{enumerate\}//g;
1909 s/\\end\{enumerate\}//g;
1910 s/\\textbf\{(.*)\}/$1/g;
1917 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1919 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1924 s/\\\\\*?\s*$/\n/; # dubious
1925 s/\\hyphenation\{[\w\s\-]+}//;
1929 'coupon' => sub { "" },
1934 # hashes for differing output formats
1935 my %nbsps = ( 'latex' => '~',
1936 'html' => '', # '&nbps;' would be nice
1937 'template' => '', # not used
1939 my $nbsp = $nbsps{$format};
1941 my %escape_functions = ( 'latex' => \&_latex_escape,
1942 'html' => \&encode_entities,
1943 'template' => sub { shift },
1945 my $escape_function = $escape_functions{$format};
1947 my %date_formats = ( 'latex' => '%b %o, %Y',
1948 'html' => '%b %o, %Y',
1951 my $date_format = $date_formats{$format};
1953 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1955 'html' => sub { return '<b>'. shift(). '</b>'
1957 'template' => sub { shift },
1959 my $embolden_function = $embolden_functions{$format};
1962 # generate template variables
1965 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1969 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1975 $returnaddress = join("\n",
1976 $conf->config_orbase("invoice_${format}returnaddress", $template)
1979 } elsif ( grep /\S/,
1980 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1982 my $convert_map = $convert_maps{$format}{'returnaddress'};
1985 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1990 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1992 my $convert_map = $convert_maps{$format}{'returnaddress'};
1993 $returnaddress = join( "\n", &$convert_map(
1994 map { s/( {2,})/'~' x length($1)/eg;
1998 ( $conf->config('company_name', $self->cust_main->agentnum),
1999 $conf->config('company_address', $self->cust_main->agentnum),
2006 my $warning = "Couldn't find a return address; ".
2007 "do you need to set the company_address configuration value?";
2009 $returnaddress = $nbsp;
2010 #$returnaddress = $warning;
2014 my %invoice_data = (
2015 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2016 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2017 'custnum' => $cust_main->display_custnum,
2018 'invnum' => $self->invnum,
2019 'date' => time2str($date_format, $self->_date),
2020 'today' => time2str('%b %o, %Y', $today),
2021 'agent' => &$escape_function($cust_main->agent->agent),
2022 'agent_custid' => &$escape_function($cust_main->agent_custid),
2023 'payname' => &$escape_function($cust_main->payname),
2024 'company' => &$escape_function($cust_main->company),
2025 'address1' => &$escape_function($cust_main->address1),
2026 'address2' => &$escape_function($cust_main->address2),
2027 'city' => &$escape_function($cust_main->city),
2028 'state' => &$escape_function($cust_main->state),
2029 'zip' => &$escape_function($cust_main->zip),
2030 'fax' => &$escape_function($cust_main->fax),
2031 'returnaddress' => $returnaddress,
2033 'terms' => $self->terms,
2034 'template' => $template, #params{'template'},
2035 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
2036 # better hang on to conf_dir for a while
2037 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2040 'current_charges' => sprintf("%.2f", $self->charged),
2041 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
2042 'ship_enable' => $conf->exists('invoice-ship_address'),
2043 'unitprices' => $conf->exists('invoice-unitprice'),
2046 my $countrydefault = $conf->config('countrydefault') || 'US';
2047 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2048 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2049 my $method = $prefix.$_;
2050 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2052 $invoice_data{'ship_country'} = ''
2053 if ( $invoice_data{'ship_country'} eq $countrydefault );
2055 $invoice_data{'cid'} = $params{'cid'}
2058 if ( $cust_main->country eq $countrydefault ) {
2059 $invoice_data{'country'} = '';
2061 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2065 $invoice_data{'address'} = \@address;
2067 $cust_main->payname.
2068 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2069 ? " (P.O. #". $cust_main->payinfo. ")"
2073 push @address, $cust_main->company
2074 if $cust_main->company;
2075 push @address, $cust_main->address1;
2076 push @address, $cust_main->address2
2077 if $cust_main->address2;
2079 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2080 push @address, $invoice_data{'country'}
2081 if $invoice_data{'country'};
2083 while (scalar(@address) < 5);
2085 $invoice_data{'logo_file'} = $params{'logo_file'}
2086 if $params{'logo_file'};
2088 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2089 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2090 #my $balance_due = $self->owed + $pr_total - $cr_total;
2091 my $balance_due = $self->owed + $pr_total;
2092 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2093 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2095 my $agentnum = $self->cust_main->agentnum;
2097 #do variable substitution in notes, footer, smallfooter
2098 foreach my $include (qw( notes footer smallfooter coupon )) {
2100 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2103 if ( $conf->exists($inc_file, $agentnum)
2104 && length( $conf->config($inc_file, $agentnum) ) ) {
2106 @inc_src = $conf->config($inc_file, $agentnum);
2110 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2112 my $convert_map = $convert_maps{$format}{$include};
2114 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2115 s/--\@\]/$delimiters{$format}[1]/g;
2118 &$convert_map( $conf->config($inc_file, $agentnum) );
2122 my $inc_tt = new Text::Template (
2124 SOURCE => [ map "$_\n", @inc_src ],
2125 DELIMITERS => $delimiters{$format},
2126 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2128 unless ( $inc_tt->compile() ) {
2129 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2130 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2134 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2136 $invoice_data{$include} =~ s/\n+$//
2137 if ($format eq 'latex');
2140 $invoice_data{'po_line'} =
2141 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2142 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2145 my %money_chars = ( 'latex' => '',
2146 'html' => $conf->config('money_char') || '$',
2149 my $money_char = $money_chars{$format};
2151 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2152 'html' => $conf->config('money_char') || '$',
2155 my $other_money_char = $other_money_chars{$format};
2157 my @detail_items = ();
2158 my @total_items = ();
2162 $invoice_data{'detail_items'} = \@detail_items;
2163 $invoice_data{'total_items'} = \@total_items;
2164 $invoice_data{'buf'} = \@buf;
2165 $invoice_data{'sections'} = \@sections;
2167 my $previous_section = { 'description' => 'Previous Charges',
2168 'subtotal' => $other_money_char.
2169 sprintf('%.2f', $pr_total),
2173 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2174 'subtotal' => $taxtotal }; # adjusted below
2176 my $adjusttotal = 0;
2177 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2178 'subtotal' => 0 }; # adjusted below
2180 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2181 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2182 my $late_sections = [];
2183 if ( $multisection ) {
2184 push @sections, $self->_items_sections( $late_sections );
2186 push @sections, { 'description' => '', 'subtotal' => '' };
2189 unless ( $conf->exists('disable_previous_balance')
2190 || $conf->exists('previous_balance-summary_only')
2194 foreach my $line_item ( $self->_items_previous ) {
2197 ext_description => [],
2199 $detail->{'ref'} = $line_item->{'pkgnum'};
2200 $detail->{'quantity'} = 1;
2201 $detail->{'section'} = $previous_section;
2202 $detail->{'description'} = &$escape_function($line_item->{'description'});
2203 if ( exists $line_item->{'ext_description'} ) {
2204 @{$detail->{'ext_description'}} = map {
2205 &$escape_function($_);
2206 } @{$line_item->{'ext_description'}};
2208 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2209 $line_item->{'amount'};
2210 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2212 push @detail_items, $detail;
2213 push @buf, [ $detail->{'description'},
2214 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2220 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2221 push @buf, ['','-----------'];
2222 push @buf, [ 'Total Previous Balance',
2223 $money_char. sprintf("%10.2f", $pr_total) ];
2227 foreach my $section (@sections, @$late_sections) {
2229 $section->{'subtotal'} = $other_money_char.
2230 sprintf('%.2f', $section->{'subtotal'})
2233 if ( $section->{'description'} ) {
2234 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2240 $options{'section'} = $section if $multisection;
2241 $options{'format'} = $format;
2242 $options{'escape_function'} = $escape_function;
2243 $options{'format_function'} = sub { () } unless $unsquelched;
2244 $options{'unsquelched'} = $unsquelched;
2246 foreach my $line_item ( $self->_items_pkg(%options) ) {
2248 ext_description => [],
2250 $detail->{'ref'} = $line_item->{'pkgnum'};
2251 $detail->{'quantity'} = $line_item->{'quantity'};
2252 $detail->{'section'} = $section;
2253 $detail->{'description'} = &$escape_function($line_item->{'description'});
2254 if ( exists $line_item->{'ext_description'} ) {
2255 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2257 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2258 $line_item->{'amount'};
2259 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2260 $line_item->{'unit_amount'};
2261 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2263 push @detail_items, $detail;
2264 push @buf, ( [ $detail->{'description'},
2265 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2267 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2271 if ( $section->{'description'} ) {
2272 push @buf, ( ['','-----------'],
2273 [ $section->{'description'}. ' sub-total',
2274 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2283 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2284 unshift @sections, $previous_section if $pr_total;
2287 foreach my $tax ( $self->_items_tax ) {
2289 $taxtotal += $tax->{'amount'};
2291 my $description = &$escape_function( $tax->{'description'} );
2292 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2294 if ( $multisection ) {
2296 my $money = $old_latex ? '' : $money_char;
2297 push @detail_items, {
2298 ext_description => [],
2301 description => $description,
2302 amount => $money. $amount,
2304 section => $tax_section,
2309 push @total_items, {
2310 'total_item' => $description,
2311 'total_amount' => $other_money_char. $amount,
2316 push @buf,[ $description,
2317 $money_char. $amount,
2324 $total->{'total_item'} = 'Sub-total';
2325 $total->{'total_amount'} =
2326 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2328 if ( $multisection ) {
2329 $tax_section->{'subtotal'} = $other_money_char.
2330 sprintf('%.2f', $taxtotal);
2331 $tax_section->{'pretotal'} = 'New charges sub-total '.
2332 $total->{'total_amount'};
2333 push @sections, $tax_section if $taxtotal;
2335 unshift @total_items, $total;
2338 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2340 push @buf,['','-----------'];
2341 push @buf,[( $conf->exists('disable_previous_balance')
2343 : 'Total New Charges'
2345 $money_char. sprintf("%10.2f",$self->charged) ];
2350 $total->{'total_item'} = &$embolden_function('Total');
2351 $total->{'total_amount'} =
2352 &$embolden_function(
2355 $self->charged + ( $conf->exists('disable_previous_balance')
2361 if ( $multisection ) {
2362 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2363 sprintf('%.2f', $self->charged );
2365 push @total_items, $total;
2367 push @buf,['','-----------'];
2368 push @buf,['Total Charges',
2370 sprintf( '%10.2f', $self->charged +
2371 ( $conf->exists('disable_previous_balance')
2380 unless ( $conf->exists('disable_previous_balance') ) {
2381 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2384 my $credittotal = 0;
2385 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2388 $total->{'total_item'} = &$escape_function($credit->{'description'});
2389 $credittotal += $credit->{'amount'};
2390 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2391 $adjusttotal += $credit->{'amount'};
2392 if ( $multisection ) {
2393 my $money = $old_latex ? '' : $money_char;
2394 push @detail_items, {
2395 ext_description => [],
2398 description => &$escape_function($credit->{'description'}),
2399 amount => $money. $credit->{'amount'},
2401 section => $adjust_section,
2404 push @total_items, $total;
2408 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2411 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2412 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2416 my $paymenttotal = 0;
2417 foreach my $payment ( $self->_items_payments ) {
2419 $total->{'total_item'} = &$escape_function($payment->{'description'});
2420 $paymenttotal += $payment->{'amount'};
2421 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2422 $adjusttotal += $payment->{'amount'};
2423 if ( $multisection ) {
2424 my $money = $old_latex ? '' : $money_char;
2425 push @detail_items, {
2426 ext_description => [],
2429 description => &$escape_function($payment->{'description'}),
2430 amount => $money. $payment->{'amount'},
2432 section => $adjust_section,
2435 push @total_items, $total;
2437 push @buf, [ $payment->{'description'},
2438 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2441 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2443 if ( $multisection ) {
2444 $adjust_section->{'subtotal'} = $other_money_char.
2445 sprintf('%.2f', $adjusttotal);
2446 push @sections, $adjust_section;
2451 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2452 $total->{'total_amount'} =
2453 &$embolden_function(
2454 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2456 if ( $multisection ) {
2457 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2458 $total->{'total_amount'};
2460 push @total_items, $total;
2462 push @buf,['','-----------'];
2463 push @buf,[$self->balance_due_msg, $money_char.
2464 sprintf("%10.2f", $balance_due ) ];
2468 if ( $multisection ) {
2469 push @sections, @$late_sections
2475 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2476 /invoice_lines\((\d*)\)/;
2477 $invoice_lines += $1 || scalar(@buf);
2480 die "no invoice_lines() functions in template?"
2481 if ( $format eq 'template' && !$wasfunc );
2483 if ($format eq 'template') {
2485 if ( $invoice_lines ) {
2486 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2487 $invoice_data{'total_pages'}++
2488 if scalar(@buf) % $invoice_lines;
2491 #setup subroutine for the template
2492 sub FS::cust_bill::_template::invoice_lines {
2493 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2495 scalar(@FS::cust_bill::_template::buf)
2496 ? shift @FS::cust_bill::_template::buf
2505 push @collect, split("\n",
2506 $text_template->fill_in( HASH => \%invoice_data,
2507 PACKAGE => 'FS::cust_bill::_template'
2510 $FS::cust_bill::_template::page++;
2512 map "$_\n", @collect;
2514 warn "filling in template for invoice ". $self->invnum. "\n"
2516 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2519 $text_template->fill_in(HASH => \%invoice_data);
2523 =item print_ps [ TIME [ , TEMPLATE ] ]
2525 Returns an postscript invoice, as a scalar.
2527 TIME an optional value used to control the printing of overdue messages. The
2528 default is now. It isn't the date of the invoice; that's the `_date' field.
2529 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2530 L<Time::Local> and L<Date::Parse> for conversion functions.
2537 my ($file, $lfile) = $self->print_latex(@_);
2538 my $ps = generate_ps($file);
2544 =item print_pdf [ TIME [ , TEMPLATE ] ]
2546 Returns an PDF invoice, as a scalar.
2548 TIME an optional value used to control the printing of overdue messages. The
2549 default is now. It isn't the date of the invoice; that's the `_date' field.
2550 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2551 L<Time::Local> and L<Date::Parse> for conversion functions.
2558 my ($file, $lfile) = $self->print_latex(@_);
2559 my $pdf = generate_pdf($file);
2565 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2567 Returns an HTML invoice, as a scalar.
2569 TIME an optional value used to control the printing of overdue messages. The
2570 default is now. It isn't the date of the invoice; that's the `_date' field.
2571 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2572 L<Time::Local> and L<Date::Parse> for conversion functions.
2574 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2575 when emailing the invoice as part of a multipart/related MIME email.
2583 %params = %{ shift() };
2585 $params{'time'} = shift;
2586 $params{'template'} = shift;
2587 $params{'cid'} = shift;
2590 $params{'format'} = 'html';
2592 $self->print_generic( %params );
2595 # quick subroutine for print_latex
2597 # There are ten characters that LaTeX treats as special characters, which
2598 # means that they do not simply typeset themselves:
2599 # # $ % & ~ _ ^ \ { }
2601 # TeX ignores blanks following an escaped character; if you want a blank (as
2602 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2606 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2607 $value =~ s/([<>])/\$$1\$/g;
2611 #utility methods for print_*
2613 sub _translate_old_latex_format {
2614 warn "_translate_old_latex_format called\n"
2621 if ( $line =~ /^%%Detail\s*$/ ) {
2623 push @template, q![@--!,
2624 q! foreach my $_tr_line (@detail_items) {!,
2625 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2626 q! $_tr_line->{'description'} .= !,
2627 q! "\\tabularnewline\n~~".!,
2628 q! join( "\\tabularnewline\n~~",!,
2629 q! @{$_tr_line->{'ext_description'}}!,
2633 while ( ( my $line_item_line = shift )
2634 !~ /^%%EndDetail\s*$/ ) {
2635 $line_item_line =~ s/'/\\'/g; # nice LTS
2636 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2637 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2638 push @template, " \$OUT .= '$line_item_line';";
2641 push @template, '}',
2644 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2646 push @template, '[@--',
2647 ' foreach my $_tr_line (@total_items) {';
2649 while ( ( my $total_item_line = shift )
2650 !~ /^%%EndTotalDetails\s*$/ ) {
2651 $total_item_line =~ s/'/\\'/g; # nice LTS
2652 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2653 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2654 push @template, " \$OUT .= '$total_item_line';";
2657 push @template, '}',
2661 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2662 push @template, $line;
2668 warn "$_\n" foreach @template;
2677 #check for an invoice- specific override (eventually)
2679 #check for a customer- specific override
2680 return $self->cust_main->invoice_terms
2681 if $self->cust_main->invoice_terms;
2683 #use configured default
2684 $conf->config('invoice_default_terms') || '';
2690 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2691 $duedate = $self->_date() + ( $1 * 86400 );
2698 $self->due_date ? time2str(shift, $self->due_date) : '';
2701 sub balance_due_msg {
2703 my $msg = 'Balance Due';
2704 return $msg unless $self->terms;
2705 if ( $self->due_date ) {
2706 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2707 } elsif ( $self->terms ) {
2708 $msg .= ' - '. $self->terms;
2713 sub balance_due_date {
2716 if ( $conf->exists('invoice_default_terms')
2717 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2718 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2723 =item invnum_date_pretty
2725 Returns a string with the invoice number and date, for example:
2726 "Invoice #54 (3/20/2008)"
2730 sub invnum_date_pretty {
2732 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2737 Returns a string with the date, for example: "3/20/2008"
2743 time2str('%x', $self->_date);
2746 sub _items_sections {
2753 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2756 if ( $cust_bill_pkg->pkgnum > 0 ) {
2757 my $usage = $cust_bill_pkg->usage;
2759 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2760 my $desc = $display->section;
2761 my $type = $display->type;
2763 if ( $display->post_total ) {
2764 if (! $type || $type eq 'S') {
2765 $l{$desc} += $cust_bill_pkg->setup
2766 if ( $cust_bill_pkg->setup != 0 );
2770 $l{$desc} += $cust_bill_pkg->recur
2771 if ( $cust_bill_pkg->recur != 0 );
2774 if ($type && $type eq 'R') {
2775 $l{$desc} += $cust_bill_pkg->recur - $usage
2776 if ( $cust_bill_pkg->recur != 0 );
2779 if ($type && $type eq 'U') {
2780 $l{$desc} += $usage;
2784 if (! $type || $type eq 'S') {
2785 $s{$desc} += $cust_bill_pkg->setup
2786 if ( $cust_bill_pkg->setup != 0 );
2790 $s{$desc} += $cust_bill_pkg->recur
2791 if ( $cust_bill_pkg->recur != 0 );
2794 if ($type && $type eq 'R') {
2795 $s{$desc} += $cust_bill_pkg->recur - $usage
2796 if ( $cust_bill_pkg->recur != 0 );
2799 if ($type && $type eq 'U') {
2800 $s{$desc} += $usage;
2811 push @$late, map { { 'description' => $_,
2812 'subtotal' => $l{$_},
2816 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2823 #my @display = scalar(@_)
2825 # : qw( _items_previous _items_pkg );
2826 # #: qw( _items_pkg );
2827 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2828 my @display = qw( _items_previous _items_pkg );
2831 foreach my $display ( @display ) {
2832 push @b, $self->$display(@_);
2837 sub _items_previous {
2839 my $cust_main = $self->cust_main;
2840 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2842 foreach ( @pr_cust_bill ) {
2844 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2845 ' ('. time2str('%x',$_->_date). ')',
2846 #'pkgpart' => 'N/A',
2848 'amount' => sprintf("%.2f", $_->owed),
2854 # 'description' => 'Previous Balance',
2855 # #'pkgpart' => 'N/A',
2856 # 'pkgnum' => 'N/A',
2857 # 'amount' => sprintf("%10.2f", $pr_total ),
2858 # 'ext_description' => [ map {
2859 # "Invoice ". $_->invnum.
2860 # " (". time2str("%x",$_->_date). ") ".
2861 # sprintf("%10.2f", $_->owed)
2862 # } @pr_cust_bill ],
2869 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2870 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2874 return 0 unless $a cmp $b;
2875 return -1 if $b eq 'Tax';
2876 return 1 if $a eq 'Tax';
2877 return -1 if $b eq 'Other surcharges';
2878 return 1 if $a eq 'Other surcharges';
2884 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2885 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2888 sub _items_cust_bill_pkg {
2890 my $cust_bill_pkg = shift;
2893 my $format = $opt{format} || '';
2894 my $escape_function = $opt{escape_function} || sub { shift };
2895 my $format_function = $opt{format_function} || '';
2896 my $unsquelched = $opt{unsquelched} || '';
2897 my $section = $opt{section}->{description} if $opt{section};
2900 my ($s, $r, $u) = ( undef, undef, undef );
2901 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2904 foreach ( $s, $r, $u ) {
2905 if ( $_ && !$cust_bill_pkg->hidden ) {
2906 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2907 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2913 foreach my $display ( grep { defined($section)
2914 ? $_->section eq $section
2917 $cust_bill_pkg->cust_bill_pkg_display
2921 my $type = $display->type;
2923 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2925 my $desc = $cust_bill_pkg->desc;
2926 $desc = substr($desc, 0, 50). '...'
2927 if $format eq 'latex' && length($desc) > 50;
2929 my %details_opt = ( 'format' => $format,
2930 'escape_function' => $escape_function,
2931 'format_function' => $format_function,
2934 if ( $cust_bill_pkg->pkgnum > 0 ) {
2936 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2938 my $description = $desc;
2939 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2942 push @d, map &{$escape_function}($_),
2943 $cust_pkg->h_labels_short($self->_date)
2944 unless $cust_pkg->part_pkg->hide_svc_detail
2945 || $cust_bill_pkg->hidden;
2946 push @d, $cust_bill_pkg->details(%details_opt)
2947 if $cust_bill_pkg->recur == 0;
2949 if ( $cust_bill_pkg->hidden ) {
2950 $s->{amount} += $cust_bill_pkg->setup;
2951 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2952 push @{ $s->{ext_description} }, @d;
2955 description => $description,
2956 #pkgpart => $part_pkg->pkgpart,
2957 pkgnum => $cust_bill_pkg->pkgnum,
2958 amount => $cust_bill_pkg->setup,
2959 unit_amount => $cust_bill_pkg->unitsetup,
2960 quantity => $cust_bill_pkg->quantity,
2961 ext_description => \@d,
2967 if ( $cust_bill_pkg->recur != 0 &&
2968 ( !$type || $type eq 'R' || $type eq 'U' )
2972 my $is_summary = $display->summary;
2973 my $description = $is_summary ? "Usage charges" : $desc;
2975 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2976 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2977 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2982 #at least until cust_bill_pkg has "past" ranges in addition to
2983 #the "future" sdate/edate ones... see #3032
2984 my @dates = ( $self->_date );
2985 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2986 push @dates, $prev->sdate if $prev;
2988 push @d, map &{$escape_function}($_),
2989 $cust_pkg->h_labels_short(@dates)
2990 #$cust_bill_pkg->edate,
2991 #$cust_bill_pkg->sdate)
2992 unless $cust_pkg->part_pkg->hide_svc_detail
2993 || $cust_bill_pkg->itemdesc
2994 || $cust_bill_pkg->hidden
2997 push @d, $cust_bill_pkg->details(%details_opt)
2998 unless ($is_summary || $type && $type eq 'R');
3002 $amount = $cust_bill_pkg->recur;
3003 }elsif($type eq 'R') {
3004 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3005 }elsif($type eq 'U') {
3006 $amount = $cust_bill_pkg->usage;
3009 if ( !$type || $type eq 'R' ) {
3011 if ( $cust_bill_pkg->hidden ) {
3012 $r->{amount} += $amount;
3013 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3014 push @{ $r->{ext_description} }, @d;
3017 description => $description,
3018 #pkgpart => $part_pkg->pkgpart,
3019 pkgnum => $cust_bill_pkg->pkgnum,
3021 unit_amount => $cust_bill_pkg->unitrecur,
3022 quantity => $cust_bill_pkg->quantity,
3023 ext_description => \@d,
3027 } elsif ( $amount ) { # && $type eq 'U'
3029 if ( $cust_bill_pkg->hidden ) {
3030 $u->{amount} += $amount;
3031 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3032 push @{ $u->{ext_description} }, @d;
3035 description => $description,
3036 #pkgpart => $part_pkg->pkgpart,
3037 pkgnum => $cust_bill_pkg->pkgnum,
3039 unit_amount => $cust_bill_pkg->unitrecur,
3040 quantity => $cust_bill_pkg->quantity,
3041 ext_description => \@d,
3047 } # recurring or usage with recurring charge
3049 } else { #pkgnum tax or one-shot line item (??)
3051 if ( $cust_bill_pkg->setup != 0 ) {
3053 'description' => $desc,
3054 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
3057 if ( $cust_bill_pkg->recur != 0 ) {
3059 'description' => "$desc (".
3060 time2str("%x", $cust_bill_pkg->sdate). ' - '.
3061 time2str("%x", $cust_bill_pkg->edate). ')',
3062 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
3072 foreach ( $s, $r, $u ) {
3074 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3075 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3084 sub _items_credits {
3085 my( $self, %opt ) = @_;
3086 my $trim_len = $opt{'trim_len'} || 60;
3090 foreach ( $self->cust_credited ) {
3092 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3094 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3095 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3096 $reason = " ($reason) " if $reason;
3099 #'description' => 'Credit ref\#'. $_->crednum.
3100 # " (". time2str("%x",$_->cust_credit->_date) .")".
3102 'description' => 'Credit applied '.
3103 time2str("%x",$_->cust_credit->_date). $reason,
3104 'amount' => sprintf("%.2f",$_->amount),
3112 sub _items_payments {
3116 #get & print payments
3117 foreach ( $self->cust_bill_pay ) {
3119 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3122 'description' => "Payment received ".
3123 time2str("%x",$_->cust_pay->_date ),
3124 'amount' => sprintf("%.2f", $_->amount )
3134 Returns an array of CSV strings representing the call details for this invoice
3140 map { $_->details( 'format_function' => sub{ shift },
3141 'escape_function' => sub{ return() },
3145 $self->cust_bill_pkg;
3155 =item process_reprint
3159 sub process_reprint {
3160 process_re_X('print', @_);
3163 =item process_reemail
3167 sub process_reemail {
3168 process_re_X('email', @_);
3176 process_re_X('fax', @_);
3184 process_re_X('ftp', @_);
3191 sub process_respool {
3192 process_re_X('spool', @_);
3195 use Storable qw(thaw);
3199 my( $method, $job ) = ( shift, shift );
3200 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3202 my $param = thaw(decode_base64(shift));
3203 warn Dumper($param) if $DEBUG;
3214 my($method, $job, %param ) = @_;
3216 warn "re_X $method for job $job with param:\n".
3217 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3220 #some false laziness w/search/cust_bill.html
3222 my $orderby = 'ORDER BY cust_bill._date';
3224 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3226 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3228 my @cust_bill = qsearch( {
3229 #'select' => "cust_bill.*",
3230 'table' => 'cust_bill',
3231 'addl_from' => $addl_from,
3233 'extra_sql' => $extra_sql,
3234 'order_by' => $orderby,
3238 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3240 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3243 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3244 foreach my $cust_bill ( @cust_bill ) {
3245 $cust_bill->$method();
3247 if ( $job ) { #progressbar foo
3249 if ( time - $min_sec > $last ) {
3250 my $error = $job->update_statustext(
3251 int( 100 * $num / scalar(@cust_bill) )
3253 die $error if $error;
3264 =head1 CLASS METHODS
3270 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3276 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3281 Returns an SQL fragment to retreive the net amount (charged minus credited).
3287 'charged - '. $class->credited_sql;
3292 Returns an SQL fragment to retreive the amount paid against this invoice.
3298 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3299 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3304 Returns an SQL fragment to retreive the amount credited against this invoice.
3310 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3311 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3314 =item search_sql HASHREF
3316 Class method which returns an SQL WHERE fragment to search for parameters
3317 specified in HASHREF. Valid parameters are
3323 Epoch date (UNIX timestamp) setting a lower bound for _date values
3327 Epoch date (UNIX timestamp) setting an upper bound for _date values
3341 =item newest_percust
3345 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3350 my($class, $param) = @_;
3352 warn "$me search_sql called with params: \n".
3353 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3358 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3359 push @search, "cust_bill._date >= $1";
3361 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3362 push @search, "cust_bill._date < $1";
3364 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3365 push @search, "cust_bill.invnum >= $1";
3367 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3368 push @search, "cust_bill.invnum <= $1";
3370 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3371 push @search, "cust_main.agentnum = $1";
3374 push @search, '0 != '. FS::cust_bill->owed_sql
3375 if $param->{'open'};
3377 push @search, '0 != '. FS::cust_bill->net_sql
3380 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3381 if $param->{'days'};
3383 if ( $param->{'newest_percust'} ) {
3385 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3386 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3388 my @newest_where = map { my $x = $_;
3389 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3392 grep ! /^cust_main./, @search;
3393 my $newest_where = scalar(@newest_where)
3394 ? ' AND '. join(' AND ', @newest_where)
3398 push @search, "cust_bill._date = (
3399 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3400 WHERE newest_cust_bill.custnum = cust_bill.custnum
3406 my $curuser = $FS::CurrentUser::CurrentUser;
3407 if ( $curuser->username eq 'fs_queue'
3408 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3410 my $newuser = qsearchs('access_user', {
3411 'username' => $username,
3415 $curuser = $newuser;
3417 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3421 push @search, $curuser->agentnums_sql;
3423 join(' AND ', @search );
3435 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3436 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base