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',
240 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
247 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
249 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
252 =item open_cust_bill_pkg
254 Returns the open line items for this invoice.
256 Note that cust_bill_pkg with both setup and recur fees are returned as two
257 separate line items, each with only one fee.
261 # modeled after cust_main::open_cust_bill
262 sub open_cust_bill_pkg {
265 # grep { $_->owed > 0 } $self->cust_bill_pkg
267 my %other = ( 'recur' => 'setup',
268 'setup' => 'recur', );
270 foreach my $field ( qw( recur setup )) {
271 push @open, map { $_->set( $other{$field}, 0 ); $_; }
272 grep { $_->owed($field) > 0 }
273 $self->cust_bill_pkg;
279 =item cust_bill_event
281 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
285 sub cust_bill_event {
287 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
290 =item num_cust_bill_event
292 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
296 sub num_cust_bill_event {
299 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
300 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
301 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
302 $sth->fetchrow_arrayref->[0];
307 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
311 #false laziness w/cust_pkg.pm
315 'table' => 'cust_event',
316 'addl_from' => 'JOIN part_event USING ( eventpart )',
317 'hashref' => { 'tablenum' => $self->invnum },
318 'extra_sql' => " AND eventtable = 'cust_bill' ",
324 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
328 #false laziness w/cust_pkg.pm
332 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
333 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
334 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
335 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
336 $sth->fetchrow_arrayref->[0];
341 Returns the customer (see L<FS::cust_main>) for this invoice.
347 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
350 =item cust_suspend_if_balance_over AMOUNT
352 Suspends the customer associated with this invoice if the total amount owed on
353 this invoice and all older invoices is greater than the specified amount.
355 Returns a list: an empty list on success or a list of errors.
359 sub cust_suspend_if_balance_over {
360 my( $self, $amount ) = ( shift, shift );
361 my $cust_main = $self->cust_main;
362 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
365 $cust_main->suspend(@_);
371 Depreciated. See the cust_credited method.
373 #Returns a list consisting of the total previous credited (see
374 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
375 #outstanding credits (FS::cust_credit objects).
381 croak "FS::cust_bill->cust_credit depreciated; see ".
382 "FS::cust_bill->cust_credit_bill";
385 #my @cust_credit = sort { $a->_date <=> $b->_date }
386 # grep { $_->credited != 0 && $_->_date < $self->_date }
387 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
389 #foreach (@cust_credit) { $total += $_->credited; }
390 #$total, @cust_credit;
395 Depreciated. See the cust_bill_pay method.
397 #Returns all payments (see L<FS::cust_pay>) for this invoice.
403 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
405 #sort { $a->_date <=> $b->_date }
406 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
412 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
418 sort { $a->_date <=> $b->_date }
419 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
424 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
430 sort { $a->_date <=> $b->_date }
431 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
437 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
444 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
446 foreach (@taxlines) { $total += $_->setup; }
452 Returns the amount owed (still outstanding) on this invoice, which is charged
453 minus all payment applications (see L<FS::cust_bill_pay>) and credit
454 applications (see L<FS::cust_credit_bill>).
460 my $balance = $self->charged;
461 $balance -= $_->amount foreach ( $self->cust_bill_pay );
462 $balance -= $_->amount foreach ( $self->cust_credited );
463 $balance = sprintf( "%.2f", $balance);
464 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
468 =item apply_payments_and_credits
472 sub apply_payments_and_credits {
475 local $SIG{HUP} = 'IGNORE';
476 local $SIG{INT} = 'IGNORE';
477 local $SIG{QUIT} = 'IGNORE';
478 local $SIG{TERM} = 'IGNORE';
479 local $SIG{TSTP} = 'IGNORE';
480 local $SIG{PIPE} = 'IGNORE';
482 my $oldAutoCommit = $FS::UID::AutoCommit;
483 local $FS::UID::AutoCommit = 0;
486 $self->select_for_update; #mutex
488 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
489 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
491 while ( $self->owed > 0 and ( @payments || @credits ) ) {
494 if ( @payments && @credits ) {
496 #decide which goes first by weight of top (unapplied) line item
498 my @open_lineitems = $self->open_cust_bill_pkg;
501 max( map { $_->part_pkg->pay_weight || 0 }
506 my $max_credit_weight =
507 max( map { $_->part_pkg->credit_weight || 0 }
513 #if both are the same... payments first? it has to be something
514 if ( $max_pay_weight >= $max_credit_weight ) {
520 } elsif ( @payments ) {
522 } elsif ( @credits ) {
525 die "guru meditation #12 and 35";
528 if ( $app eq 'pay' ) {
530 my $payment = shift @payments;
532 $app = new FS::cust_bill_pay {
533 'paynum' => $payment->paynum,
534 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
537 } elsif ( $app eq 'credit' ) {
539 my $credit = shift @credits;
541 $app = new FS::cust_credit_bill {
542 'crednum' => $credit->crednum,
543 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
547 die "guru meditation #12 and 35";
550 $app->invnum( $self->invnum );
552 my $error = $app->insert;
554 $dbh->rollback if $oldAutoCommit;
555 return "Error inserting ". $app->table. " record: $error";
557 die $error if $error;
561 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
566 =item generate_email OPTION => VALUE ...
574 sender address, required
578 alternate template name, optional
582 text attachment arrayref, optional
586 email subject, optional
590 Returns an argument list to be passed to L<FS::Misc::send_email>.
601 my $me = '[FS::cust_bill::generate_email]';
604 'from' => $args{'from'},
605 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
608 my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
610 if (ref($args{'to'}) eq 'ARRAY') {
611 $return{'to'} = $args{'to'};
613 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
614 $self->cust_main->invoicing_list
618 if ( $conf->exists('invoice_html') ) {
620 warn "$me creating HTML/text multipart message"
623 $return{'nobody'} = 1;
625 my $alternative = build MIME::Entity
626 'Type' => 'multipart/alternative',
627 'Encoding' => '7bit',
628 'Disposition' => 'inline'
632 if ( $conf->exists('invoice_email_pdf')
633 and scalar($conf->config('invoice_email_pdf_note')) ) {
635 warn "$me using 'invoice_email_pdf_note' in multipart message"
637 $data = [ map { $_ . "\n" }
638 $conf->config('invoice_email_pdf_note')
643 warn "$me not using 'invoice_email_pdf_note' in multipart message"
645 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
646 $data = $args{'print_text'};
648 $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
653 $alternative->attach(
654 'Type' => 'text/plain',
655 #'Encoding' => 'quoted-printable',
656 'Encoding' => '7bit',
658 'Disposition' => 'inline',
661 $args{'from'} =~ /\@([\w\.\-]+)/;
662 my $from = $1 || 'example.com';
663 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
666 my $agentnum = $self->cust_main->agentnum;
667 if ( defined($args{'template'}) && length($args{'template'})
668 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
671 $logo = 'logo_'. $args{'template'}. '.png';
675 my $image_data = $conf->config_binary( $logo, $agentnum);
677 my $image = build MIME::Entity
678 'Type' => 'image/png',
679 'Encoding' => 'base64',
680 'Data' => $image_data,
681 'Filename' => 'logo.png',
682 'Content-ID' => "<$content_id>",
685 $alternative->attach(
686 'Type' => 'text/html',
687 'Encoding' => 'quoted-printable',
688 'Data' => [ '<html>',
691 ' '. encode_entities($return{'subject'}),
694 ' <body bgcolor="#e8e8e8">',
695 $self->print_html({ time => '',
696 template => $args{'template'},
703 'Disposition' => 'inline',
704 #'Filename' => 'invoice.pdf',
707 if ( $conf->exists('invoice_email_pdf') ) {
712 # multipart/alternative
718 my $related = build MIME::Entity 'Type' => 'multipart/related',
719 'Encoding' => '7bit';
721 #false laziness w/Misc::send_email
722 $related->head->replace('Content-type',
724 '; boundary="'. $related->head->multipart_boundary. '"'.
725 '; type=multipart/alternative'
728 $related->add_part($alternative);
730 $related->add_part($image);
732 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
734 $return{'mimeparts'} = [ $related, $pdf ];
738 #no other attachment:
740 # multipart/alternative
745 $return{'content-type'} = 'multipart/related';
746 $return{'mimeparts'} = [ $alternative, $image ];
747 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
748 #$return{'disposition'} = 'inline';
754 if ( $conf->exists('invoice_email_pdf') ) {
755 warn "$me creating PDF attachment"
758 #mime parts arguments a la MIME::Entity->build().
759 $return{'mimeparts'} = [
760 { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
764 if ( $conf->exists('invoice_email_pdf')
765 and scalar($conf->config('invoice_email_pdf_note')) ) {
767 warn "$me using 'invoice_email_pdf_note'"
769 $return{'body'} = [ map { $_ . "\n" }
770 $conf->config('invoice_email_pdf_note')
775 warn "$me not using 'invoice_email_pdf_note'"
777 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
778 $return{'body'} = $args{'print_text'};
780 $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
793 Returns a list suitable for passing to MIME::Entity->build(), representing
794 this invoice as PDF attachment.
801 'Type' => 'application/pdf',
802 'Encoding' => 'base64',
803 'Data' => [ $self->print_pdf(@_) ],
804 'Disposition' => 'attachment',
805 'Filename' => 'invoice-'. $self->invnum. '.pdf',
809 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
811 Sends this invoice to the destinations configured for this customer: sends
812 email, prints and/or faxes. See L<FS::cust_main_invoice>.
814 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
816 AGENTNUM, if specified, means that this invoice will only be sent for customers
817 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
818 single agent) or an arrayref of agentnums.
820 INVOICE_FROM, if specified, overrides the default email invoice From: address.
822 AMOUNT, if specified, only sends the invoice if the total amount owed on this
823 invoice and all older invoices is greater than the specified amount.
830 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
831 or die "invalid invoice number: " . $opt{invnum};
833 my @args = ( $opt{template}, $opt{agentnum} );
834 push @args, $opt{invoice_from}
835 if exists($opt{invoice_from}) && $opt{invoice_from};
837 my $error = $self->send( @args );
838 die $error if $error;
844 my $template = scalar(@_) ? shift : '';
845 if ( scalar(@_) && $_[0] ) {
846 my $agentnums = ref($_[0]) ? shift : [ shift ];
847 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
853 : ( $self->_agent_invoice_from || #XXX should go away
854 $conf->config('invoice_from', $self->cust_main->agentnum )
857 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
860 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
862 my @invoicing_list = $self->cust_main->invoicing_list;
864 #$self->email_invoice($template, $invoice_from)
865 $self->email($template, $invoice_from)
866 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
868 #$self->print_invoice($template)
869 $self->print($template)
870 if grep { $_ eq 'POST' } @invoicing_list; #postal
872 $self->fax_invoice($template)
873 if grep { $_ eq 'FAX' } @invoicing_list; #fax
879 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
883 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
885 INVOICE_FROM, if specified, overrides the default email invoice From: address.
889 sub queueable_email {
892 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
893 or die "invalid invoice number: " . $opt{invnum};
895 my @args = ( $opt{template} );
896 push @args, $opt{invoice_from}
897 if exists($opt{invoice_from}) && $opt{invoice_from};
899 my $error = $self->email( @args );
900 die $error if $error;
907 my $template = scalar(@_) ? shift : '';
911 : ( $self->_agent_invoice_from || #XXX should go away
912 $conf->config('invoice_from', $self->cust_main->agentnum )
916 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
917 $self->cust_main->invoicing_list;
919 #better to notify this person than silence
920 @invoicing_list = ($invoice_from) unless @invoicing_list;
922 my $subject = $self->email_subject($template);
924 my $error = send_email(
925 $self->generate_email(
926 'from' => $invoice_from,
927 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
928 'subject' => $subject,
929 'template' => $template,
932 die "can't email invoice: $error\n" if $error;
933 #die "$error\n" if $error;
940 #my $template = scalar(@_) ? shift : '';
943 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
946 my $cust_main = $self->cust_main;
947 my $name = $cust_main->name;
948 my $name_short = $cust_main->name_short;
949 my $invoice_number = $self->invnum;
950 my $invoice_date = $self->_date_pretty;
955 =item lpr_data [ TEMPLATENAME ]
957 Returns the postscript or plaintext for this invoice as an arrayref.
959 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
964 my( $self, $template) = @_;
965 $conf->exists('invoice_latex')
966 ? [ $self->print_ps('', $template) ]
967 : [ $self->print_text('', $template) ];
970 =item print [ TEMPLATENAME ]
974 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
981 my $template = scalar(@_) ? shift : '';
983 do_print $self->lpr_data($template);
986 =item fax_invoice [ TEMPLATENAME ]
990 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
996 my $template = scalar(@_) ? shift : '';
998 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
999 unless $conf->exists('invoice_latex');
1001 my $dialstring = $self->cust_main->getfield('fax');
1004 my $error = send_fax( 'docdata' => $self->lpr_data($template),
1005 'dialstring' => $dialstring,
1007 die $error if $error;
1011 =item ftp_invoice [ TEMPLATENAME ]
1013 Sends this invoice data via FTP.
1015 TEMPLATENAME is unused?
1021 my $template = scalar(@_) ? shift : '';
1024 'protocol' => 'ftp',
1025 'server' => $conf->config('cust_bill-ftpserver'),
1026 'username' => $conf->config('cust_bill-ftpusername'),
1027 'password' => $conf->config('cust_bill-ftppassword'),
1028 'dir' => $conf->config('cust_bill-ftpdir'),
1029 'format' => $conf->config('cust_bill-ftpformat'),
1033 =item spool_invoice [ TEMPLATENAME ]
1035 Spools this invoice data (see L<FS::spool_csv>)
1037 TEMPLATENAME is unused?
1043 my $template = scalar(@_) ? shift : '';
1046 'format' => $conf->config('cust_bill-spoolformat'),
1047 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1051 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1053 Like B<send>, but only sends the invoice if it is the newest open invoice for
1058 sub send_if_newest {
1063 grep { $_->owed > 0 }
1064 qsearch('cust_bill', {
1065 'custnum' => $self->custnum,
1066 #'_date' => { op=>'>', value=>$self->_date },
1067 'invnum' => { op=>'>', value=>$self->invnum },
1074 =item send_csv OPTION => VALUE, ...
1076 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1080 protocol - currently only "ftp"
1086 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1087 and YYMMDDHHMMSS is a timestamp.
1089 See L</print_csv> for a description of the output format.
1094 my($self, %opt) = @_;
1098 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1099 mkdir $spooldir, 0700 unless -d $spooldir;
1101 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1102 my $file = "$spooldir/$tracctnum.csv";
1104 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1106 open(CSV, ">$file") or die "can't open $file: $!";
1114 if ( $opt{protocol} eq 'ftp' ) {
1115 eval "use Net::FTP;";
1117 $net = Net::FTP->new($opt{server}) or die @$;
1119 die "unknown protocol: $opt{protocol}";
1122 $net->login( $opt{username}, $opt{password} )
1123 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1125 $net->binary or die "can't set binary mode";
1127 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1129 $net->put($file) or die "can't put $file: $!";
1139 Spools CSV invoice data.
1145 =item format - 'default' or 'billco'
1147 =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>).
1149 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1151 =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.
1158 my($self, %opt) = @_;
1160 my $cust_main = $self->cust_main;
1162 if ( $opt{'dest'} ) {
1163 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1164 $cust_main->invoicing_list;
1165 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1166 || ! keys %invoicing_list;
1169 if ( $opt{'balanceover'} ) {
1171 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1174 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1175 mkdir $spooldir, 0700 unless -d $spooldir;
1177 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1181 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1182 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1185 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1187 open(CSV, ">>$file") or die "can't open $file: $!";
1188 flock(CSV, LOCK_EX);
1193 if ( lc($opt{'format'}) eq 'billco' ) {
1195 flock(CSV, LOCK_UN);
1200 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1203 open(CSV,">>$file") or die "can't open $file: $!";
1204 flock(CSV, LOCK_EX);
1210 flock(CSV, LOCK_UN);
1217 =item print_csv OPTION => VALUE, ...
1219 Returns CSV data for this invoice.
1223 format - 'default' or 'billco'
1225 Returns a list consisting of two scalars. The first is a single line of CSV
1226 header information for this invoice. The second is one or more lines of CSV
1227 detail information for this invoice.
1229 If I<format> is not specified or "default", the fields of the CSV file are as
1232 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1236 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1238 B<record_type> is C<cust_bill> for the initial header line only. The
1239 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1240 fields are filled in.
1242 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1243 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1246 =item invnum - invoice number
1248 =item custnum - customer number
1250 =item _date - invoice date
1252 =item charged - total invoice amount
1254 =item first - customer first name
1256 =item last - customer first name
1258 =item company - company name
1260 =item address1 - address line 1
1262 =item address2 - address line 1
1272 =item pkg - line item description
1274 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1276 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1278 =item sdate - start date for recurring fee
1280 =item edate - end date for recurring fee
1284 If I<format> is "billco", the fields of the header CSV file are as follows:
1286 +-------------------------------------------------------------------+
1287 | FORMAT HEADER FILE |
1288 |-------------------------------------------------------------------|
1289 | Field | Description | Name | Type | Width |
1290 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1291 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1292 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1293 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1294 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1295 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1296 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1297 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1298 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1299 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1300 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1301 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1302 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1303 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1304 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1305 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1306 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1307 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1308 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1309 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1310 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1311 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1312 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1313 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1314 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1315 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1316 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1317 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1318 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1319 +-------+-------------------------------+------------+------+-------+
1321 If I<format> is "billco", the fields of the detail CSV file are as follows:
1323 FORMAT FOR DETAIL FILE
1325 Field | Description | Name | Type | Width
1326 1 | N/A-Leave Empty | RC | CHAR | 2
1327 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1328 3 | Account Number | TRACCTNUM | CHAR | 15
1329 4 | Invoice Number | TRINVOICE | CHAR | 15
1330 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1331 6 | Transaction Detail | DETAILS | CHAR | 100
1332 7 | Amount | AMT | NUM* | 9
1333 8 | Line Format Control** | LNCTRL | CHAR | 2
1334 9 | Grouping Code | GROUP | CHAR | 2
1335 10 | User Defined | ACCT CODE | CHAR | 15
1340 my($self, %opt) = @_;
1342 eval "use Text::CSV_XS";
1345 my $cust_main = $self->cust_main;
1347 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1349 if ( lc($opt{'format'}) eq 'billco' ) {
1352 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1354 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1356 my( $previous_balance, @unused ) = $self->previous; #previous balance
1358 my $pmt_cr_applied = 0;
1359 $pmt_cr_applied += $_->{'amount'}
1360 foreach ( $self->_items_payments, $self->_items_credits ) ;
1362 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1365 '', # 1 | N/A-Leave Empty CHAR 2
1366 '', # 2 | N/A-Leave Empty CHAR 15
1367 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1368 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1369 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1370 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1371 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1372 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1373 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1374 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1375 '', # 10 | Ancillary Billing Information CHAR 30
1376 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1377 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1380 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1383 $duedate, # 14 | Bill Due Date CHAR 10
1385 $previous_balance, # 15 | Previous Balance NUM* 9
1386 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1387 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1388 $totaldue, # 18 | Total Amt Due NUM* 9
1389 $totaldue, # 19 | Total Amt Due NUM* 9
1390 '', # 20 | 30 Day Aging NUM* 9
1391 '', # 21 | 60 Day Aging NUM* 9
1392 '', # 22 | 90 Day Aging NUM* 9
1393 'N', # 23 | Y/N CHAR 1
1394 '', # 24 | Remittance automation CHAR 100
1395 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1396 $self->custnum, # 26 | Customer Reference Number CHAR 15
1397 '0', # 27 | Federal Tax*** NUM* 9
1398 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1399 '0', # 29 | Other Taxes & Fees*** NUM* 9
1408 time2str("%x", $self->_date),
1409 sprintf("%.2f", $self->charged),
1410 ( map { $cust_main->getfield($_) }
1411 qw( first last company address1 address2 city state zip country ) ),
1413 ) or die "can't create csv";
1416 my $header = $csv->string. "\n";
1419 if ( lc($opt{'format'}) eq 'billco' ) {
1422 foreach my $item ( $self->_items_pkg ) {
1425 '', # 1 | N/A-Leave Empty CHAR 2
1426 '', # 2 | N/A-Leave Empty CHAR 15
1427 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1428 $self->invnum, # 4 | Invoice Number CHAR 15
1429 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1430 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1431 $item->{'amount'}, # 7 | Amount NUM* 9
1432 '', # 8 | Line Format Control** CHAR 2
1433 '', # 9 | Grouping Code CHAR 2
1434 '', # 10 | User Defined CHAR 15
1437 $detail .= $csv->string. "\n";
1443 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1445 my($pkg, $setup, $recur, $sdate, $edate);
1446 if ( $cust_bill_pkg->pkgnum ) {
1448 ($pkg, $setup, $recur, $sdate, $edate) = (
1449 $cust_bill_pkg->part_pkg->pkg,
1450 ( $cust_bill_pkg->setup != 0
1451 ? sprintf("%.2f", $cust_bill_pkg->setup )
1453 ( $cust_bill_pkg->recur != 0
1454 ? sprintf("%.2f", $cust_bill_pkg->recur )
1456 ( $cust_bill_pkg->sdate
1457 ? time2str("%x", $cust_bill_pkg->sdate)
1459 ($cust_bill_pkg->edate
1460 ?time2str("%x", $cust_bill_pkg->edate)
1464 } else { #pkgnum tax
1465 next unless $cust_bill_pkg->setup != 0;
1466 $pkg = $cust_bill_pkg->desc;
1467 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1468 ( $sdate, $edate ) = ( '', '' );
1474 ( map { '' } (1..11) ),
1475 ($pkg, $setup, $recur, $sdate, $edate)
1476 ) or die "can't create csv";
1478 $detail .= $csv->string. "\n";
1484 ( $header, $detail );
1490 Pays this invoice with a compliemntary payment. If there is an error,
1491 returns the error, otherwise returns false.
1497 my $cust_pay = new FS::cust_pay ( {
1498 'invnum' => $self->invnum,
1499 'paid' => $self->owed,
1502 'payinfo' => $self->cust_main->payinfo,
1510 Attempts to pay this invoice with a credit card payment via a
1511 Business::OnlinePayment realtime gateway. See
1512 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1513 for supported processors.
1519 $self->realtime_bop( 'CC', @_ );
1524 Attempts to pay this invoice with an electronic check (ACH) payment via a
1525 Business::OnlinePayment realtime gateway. See
1526 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1527 for supported processors.
1533 $self->realtime_bop( 'ECHECK', @_ );
1538 Attempts to pay this invoice with phone bill (LEC) payment via a
1539 Business::OnlinePayment realtime gateway. See
1540 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1541 for supported processors.
1547 $self->realtime_bop( 'LEC', @_ );
1551 my( $self, $method ) = @_;
1553 my $cust_main = $self->cust_main;
1554 my $balance = $cust_main->balance;
1555 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1556 $amount = sprintf("%.2f", $amount);
1557 return "not run (balance $balance)" unless $amount > 0;
1559 my $description = 'Internet Services';
1560 if ( $conf->exists('business-onlinepayment-description') ) {
1561 my $dtempl = $conf->config('business-onlinepayment-description');
1563 my $agent_obj = $cust_main->agent
1564 or die "can't retreive agent for $cust_main (agentnum ".
1565 $cust_main->agentnum. ")";
1566 my $agent = $agent_obj->agent;
1567 my $pkgs = join(', ',
1568 map { $_->part_pkg->pkg }
1569 grep { $_->pkgnum } $self->cust_bill_pkg
1571 $description = eval qq("$dtempl");
1574 $cust_main->realtime_bop($method, $amount,
1575 'description' => $description,
1576 'invnum' => $self->invnum,
1581 =item batch_card OPTION => VALUE...
1583 Adds a payment for this invoice to the pending credit card batch (see
1584 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1585 runs the payment using a realtime gateway.
1590 my ($self, %options) = @_;
1591 my $cust_main = $self->cust_main;
1593 $options{invnum} = $self->invnum;
1595 $cust_main->batch_card(%options);
1598 sub _agent_template {
1600 $self->cust_main->agent_template;
1603 sub _agent_invoice_from {
1605 $self->cust_main->agent_invoice_from;
1608 =item print_text [ TIME [ , TEMPLATE ] ]
1610 Returns an text invoice, as a list of lines.
1612 TIME an optional value used to control the printing of overdue messages. The
1613 default is now. It isn't the date of the invoice; that's the `_date' field.
1614 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1615 L<Time::Local> and L<Date::Parse> for conversion functions.
1620 my( $self, $today, $template, %opt ) = @_;
1622 my %params = ( 'format' => 'template' );
1623 $params{'time'} = $today if $today;
1624 $params{'template'} = $template if $template;
1625 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1627 $self->print_generic( %params );
1630 =item print_latex [ TIME [ , TEMPLATE ] ]
1632 Internal method - returns a filename of a filled-in LaTeX template for this
1633 invoice (Note: add ".tex" to get the actual filename), and a filename of
1634 an associated logo (with the .eps extension included).
1636 See print_ps and print_pdf for methods that return PostScript and PDF output.
1638 TIME an optional value used to control the printing of overdue messages. The
1639 default is now. It isn't the date of the invoice; that's the `_date' field.
1640 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1641 L<Time::Local> and L<Date::Parse> for conversion functions.
1646 my( $self, $today, $template, %opt ) = @_;
1648 my %params = ( 'format' => 'latex' );
1649 $params{'time'} = $today if $today;
1650 $params{'template'} = $template if $template;
1651 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1653 $template ||= $self->_agent_template;
1655 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1656 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1660 ) or die "can't open temp file: $!\n";
1662 my $agentnum = $self->cust_main->agentnum;
1664 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1665 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1666 or die "can't write temp file: $!\n";
1668 print $lh $conf->config_binary('logo.eps', $agentnum)
1669 or die "can't write temp file: $!\n";
1672 $params{'logo_file'} = $lh->filename;
1674 my @filled_in = $self->print_generic( %params );
1676 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1680 ) or die "can't open temp file: $!\n";
1681 print $fh join('', @filled_in );
1684 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1685 return ($1, $params{'logo_file'});
1689 =item print_generic OPTIONS_HASH
1691 Internal method - returns a filled-in template for this invoice as a scalar.
1693 See print_ps and print_pdf for methods that return PostScript and PDF output.
1695 Non optional options include
1696 format - latex, html, template
1698 Optional options include
1700 template - a value used as a suffix for a configuration template
1702 time - a value used to control the printing of overdue messages. The
1703 default is now. It isn't the date of the invoice; that's the `_date' field.
1704 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1705 L<Time::Local> and L<Date::Parse> for conversion functions.
1709 unsquelch_cdr - overrides any per customer cdr squelching when true
1713 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1714 # (alignment?) problems to change them all to '%.2f' ?
1717 my( $self, %params ) = @_;
1718 my $today = $params{today} ? $params{today} : time;
1719 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1722 my $format = $params{format};
1723 die "Unknown format: $format"
1724 unless $format =~ /^(latex|html|template)$/;
1726 my $cust_main = $self->cust_main;
1727 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1728 unless $cust_main->payname
1729 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1731 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1732 'html' => [ '<%=', '%>' ],
1733 'template' => [ '{', '}' ],
1736 #create the template
1737 my $template = $params{template} ? $params{template} : $self->_agent_template;
1738 my $templatefile = "invoice_$format";
1739 $templatefile .= "_$template"
1740 if length($template);
1741 my @invoice_template = map "$_\n", $conf->config($templatefile)
1742 or die "cannot load config data $templatefile";
1745 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1746 #change this to a die when the old code is removed
1747 warn "old-style invoice template $templatefile; ".
1748 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1749 $old_latex = 'true';
1750 @invoice_template = _translate_old_latex_format(@invoice_template);
1753 my $text_template = new Text::Template(
1755 SOURCE => \@invoice_template,
1756 DELIMITERS => $delimiters{$format},
1759 $text_template->compile()
1760 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1763 # additional substitution could possibly cause breakage in existing templates
1764 my %convert_maps = (
1766 'notes' => sub { map "$_", @_ },
1767 'footer' => sub { map "$_", @_ },
1768 'smallfooter' => sub { map "$_", @_ },
1769 'returnaddress' => sub { map "$_", @_ },
1770 'coupon' => sub { map "$_", @_ },
1776 s/%%(.*)$/<!-- $1 -->/g;
1777 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1778 s/\\begin\{enumerate\}/<ol>/g;
1780 s/\\end\{enumerate\}/<\/ol>/g;
1781 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1790 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1792 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1797 s/\\\\\*?\s*$/<BR>/;
1798 s/\\hyphenation\{[\w\s\-]+}//;
1803 'coupon' => sub { "" },
1810 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1811 s/\\begin\{enumerate\}//g;
1813 s/\\end\{enumerate\}//g;
1814 s/\\textbf\{(.*)\}/$1/g;
1821 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1823 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1828 s/\\\\\*?\s*$/\n/; # dubious
1829 s/\\hyphenation\{[\w\s\-]+}//;
1833 'coupon' => sub { "" },
1838 # hashes for differing output formats
1839 my %nbsps = ( 'latex' => '~',
1840 'html' => '', # '&nbps;' would be nice
1841 'template' => '', # not used
1843 my $nbsp = $nbsps{$format};
1845 my %escape_functions = ( 'latex' => \&_latex_escape,
1846 'html' => \&encode_entities,
1847 'template' => sub { shift },
1849 my $escape_function = $escape_functions{$format};
1851 my %date_formats = ( 'latex' => '%b %o, %Y',
1852 'html' => '%b %o, %Y',
1855 my $date_format = $date_formats{$format};
1857 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1859 'html' => sub { return '<b>'. shift(). '</b>'
1861 'template' => sub { shift },
1863 my $embolden_function = $embolden_functions{$format};
1866 # generate template variables
1869 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1873 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1879 $returnaddress = join("\n",
1880 $conf->config_orbase("invoice_${format}returnaddress", $template)
1883 } elsif ( grep /\S/,
1884 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1886 my $convert_map = $convert_maps{$format}{'returnaddress'};
1889 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1894 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1896 my $convert_map = $convert_maps{$format}{'returnaddress'};
1897 $returnaddress = join( "\n", &$convert_map(
1898 map { s/( {2,})/'~' x length($1)/eg;
1902 ( $conf->config('company_name', $self->cust_main->agentnum),
1903 $conf->config('company_address', $self->cust_main->agentnum),
1910 my $warning = "Couldn't find a return address; ".
1911 "do you need to set the company_address configuration value?";
1913 $returnaddress = $nbsp;
1914 #$returnaddress = $warning;
1918 my %invoice_data = (
1919 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
1920 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
1921 'custnum' => $cust_main->display_custnum,
1922 'invnum' => $self->invnum,
1923 'date' => time2str($date_format, $self->_date),
1924 'today' => time2str('%b %o, %Y', $today),
1925 'agent' => &$escape_function($cust_main->agent->agent),
1926 'agent_custid' => &$escape_function($cust_main->agent_custid),
1927 'payname' => &$escape_function($cust_main->payname),
1928 'company' => &$escape_function($cust_main->company),
1929 'address1' => &$escape_function($cust_main->address1),
1930 'address2' => &$escape_function($cust_main->address2),
1931 'city' => &$escape_function($cust_main->city),
1932 'state' => &$escape_function($cust_main->state),
1933 'zip' => &$escape_function($cust_main->zip),
1934 'fax' => &$escape_function($cust_main->fax),
1935 'returnaddress' => $returnaddress,
1937 'terms' => $self->terms,
1938 'template' => $template, #params{'template'},
1939 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1940 # better hang on to conf_dir for a while
1941 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1944 'current_charges' => sprintf("%.2f", $self->charged),
1945 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1946 'ship_enable' => $conf->exists('invoice-ship_address'),
1947 'unitprices' => $conf->exists('invoice-unitprice'),
1950 my $countrydefault = $conf->config('countrydefault') || 'US';
1951 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1952 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1953 my $method = $prefix.$_;
1954 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1956 $invoice_data{'ship_country'} = ''
1957 if ( $invoice_data{'ship_country'} eq $countrydefault );
1959 $invoice_data{'cid'} = $params{'cid'}
1962 if ( $cust_main->country eq $countrydefault ) {
1963 $invoice_data{'country'} = '';
1965 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1969 $invoice_data{'address'} = \@address;
1971 $cust_main->payname.
1972 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1973 ? " (P.O. #". $cust_main->payinfo. ")"
1977 push @address, $cust_main->company
1978 if $cust_main->company;
1979 push @address, $cust_main->address1;
1980 push @address, $cust_main->address2
1981 if $cust_main->address2;
1983 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1984 push @address, $invoice_data{'country'}
1985 if $invoice_data{'country'};
1987 while (scalar(@address) < 5);
1989 $invoice_data{'logo_file'} = $params{'logo_file'}
1990 if $params{'logo_file'};
1992 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1993 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1994 #my $balance_due = $self->owed + $pr_total - $cr_total;
1995 my $balance_due = $self->owed + $pr_total;
1996 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1997 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1999 my $agentnum = $self->cust_main->agentnum;
2001 #do variable substitution in notes, footer, smallfooter
2002 foreach my $include (qw( notes footer smallfooter coupon )) {
2004 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2007 if ( $conf->exists($inc_file, $agentnum)
2008 && length( $conf->config($inc_file, $agentnum) ) ) {
2010 @inc_src = $conf->config($inc_file, $agentnum);
2014 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2016 my $convert_map = $convert_maps{$format}{$include};
2018 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2019 s/--\@\]/$delimiters{$format}[1]/g;
2022 &$convert_map( $conf->config($inc_file, $agentnum) );
2026 my $inc_tt = new Text::Template (
2028 SOURCE => [ map "$_\n", @inc_src ],
2029 DELIMITERS => $delimiters{$format},
2030 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2032 unless ( $inc_tt->compile() ) {
2033 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2034 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2038 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2040 $invoice_data{$include} =~ s/\n+$//
2041 if ($format eq 'latex');
2044 $invoice_data{'po_line'} =
2045 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2046 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2049 my %money_chars = ( 'latex' => '',
2050 'html' => $conf->config('money_char') || '$',
2053 my $money_char = $money_chars{$format};
2055 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2056 'html' => $conf->config('money_char') || '$',
2059 my $other_money_char = $other_money_chars{$format};
2061 my @detail_items = ();
2062 my @total_items = ();
2066 $invoice_data{'detail_items'} = \@detail_items;
2067 $invoice_data{'total_items'} = \@total_items;
2068 $invoice_data{'buf'} = \@buf;
2069 $invoice_data{'sections'} = \@sections;
2071 my $previous_section = { 'description' => 'Previous Charges',
2072 'subtotal' => $other_money_char.
2073 sprintf('%.2f', $pr_total),
2077 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2078 'subtotal' => $taxtotal }; # adjusted below
2080 my $adjusttotal = 0;
2081 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2082 'subtotal' => 0 }; # adjusted below
2084 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2085 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2086 my $late_sections = [];
2087 if ( $multisection ) {
2088 push @sections, $self->_items_sections( $late_sections );
2090 push @sections, { 'description' => '', 'subtotal' => '' };
2093 unless ( $conf->exists('disable_previous_balance')
2094 || $conf->exists('previous_balance-summary_only')
2098 foreach my $line_item ( $self->_items_previous ) {
2101 ext_description => [],
2103 $detail->{'ref'} = $line_item->{'pkgnum'};
2104 $detail->{'quantity'} = 1;
2105 $detail->{'section'} = $previous_section;
2106 $detail->{'description'} = &$escape_function($line_item->{'description'});
2107 if ( exists $line_item->{'ext_description'} ) {
2108 @{$detail->{'ext_description'}} = map {
2109 &$escape_function($_);
2110 } @{$line_item->{'ext_description'}};
2112 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2113 $line_item->{'amount'};
2114 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2116 push @detail_items, $detail;
2117 push @buf, [ $detail->{'description'},
2118 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2124 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2125 push @buf, ['','-----------'];
2126 push @buf, [ 'Total Previous Balance',
2127 $money_char. sprintf("%10.2f", $pr_total) ];
2131 foreach my $section (@sections, @$late_sections) {
2133 $section->{'subtotal'} = $other_money_char.
2134 sprintf('%.2f', $section->{'subtotal'})
2137 if ( $section->{'description'} ) {
2138 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2144 $options{'section'} = $section if $multisection;
2145 $options{'format'} = $format;
2146 $options{'escape_function'} = $escape_function;
2147 $options{'format_function'} = sub { () } unless $unsquelched;
2148 $options{'unsquelched'} = $unsquelched;
2150 foreach my $line_item ( $self->_items_pkg(%options) ) {
2152 ext_description => [],
2154 $detail->{'ref'} = $line_item->{'pkgnum'};
2155 $detail->{'quantity'} = $line_item->{'quantity'};
2156 $detail->{'section'} = $section;
2157 $detail->{'description'} = &$escape_function($line_item->{'description'});
2158 if ( exists $line_item->{'ext_description'} ) {
2159 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2161 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2162 $line_item->{'amount'};
2163 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2164 $line_item->{'unit_amount'};
2165 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2167 push @detail_items, $detail;
2168 push @buf, ( [ $detail->{'description'},
2169 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2171 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2175 if ( $section->{'description'} ) {
2176 push @buf, ( ['','-----------'],
2177 [ $section->{'description'}. ' sub-total',
2178 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2187 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2188 unshift @sections, $previous_section if $pr_total;
2191 foreach my $tax ( $self->_items_tax ) {
2193 $taxtotal += $tax->{'amount'};
2195 my $description = &$escape_function( $tax->{'description'} );
2196 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2198 if ( $multisection ) {
2200 my $money = $old_latex ? '' : $money_char;
2201 push @detail_items, {
2202 ext_description => [],
2205 description => $description,
2206 amount => $money. $amount,
2208 section => $tax_section,
2213 push @total_items, {
2214 'total_item' => $description,
2215 'total_amount' => $other_money_char. $amount,
2220 push @buf,[ $description,
2221 $money_char. $amount,
2228 $total->{'total_item'} = 'Sub-total';
2229 $total->{'total_amount'} =
2230 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2232 if ( $multisection ) {
2233 $tax_section->{'subtotal'} = $other_money_char.
2234 sprintf('%.2f', $taxtotal);
2235 $tax_section->{'pretotal'} = 'New charges sub-total '.
2236 $total->{'total_amount'};
2237 push @sections, $tax_section if $taxtotal;
2239 unshift @total_items, $total;
2242 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2244 push @buf,['','-----------'];
2245 push @buf,[( $conf->exists('disable_previous_balance')
2247 : 'Total New Charges'
2249 $money_char. sprintf("%10.2f",$self->charged) ];
2254 $total->{'total_item'} = &$embolden_function('Total');
2255 $total->{'total_amount'} =
2256 &$embolden_function(
2259 $self->charged + ( $conf->exists('disable_previous_balance')
2265 if ( $multisection ) {
2266 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2267 sprintf('%.2f', $self->charged );
2269 push @total_items, $total;
2271 push @buf,['','-----------'];
2272 push @buf,['Total Charges',
2274 sprintf( '%10.2f', $self->charged +
2275 ( $conf->exists('disable_previous_balance')
2284 unless ( $conf->exists('disable_previous_balance') ) {
2285 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2288 my $credittotal = 0;
2289 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2292 $total->{'total_item'} = &$escape_function($credit->{'description'});
2293 $credittotal += $credit->{'amount'};
2294 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2295 $adjusttotal += $credit->{'amount'};
2296 if ( $multisection ) {
2297 my $money = $old_latex ? '' : $money_char;
2298 push @detail_items, {
2299 ext_description => [],
2302 description => &$escape_function($credit->{'description'}),
2303 amount => $money. $credit->{'amount'},
2305 section => $adjust_section,
2308 push @total_items, $total;
2312 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2315 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2316 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2320 my $paymenttotal = 0;
2321 foreach my $payment ( $self->_items_payments ) {
2323 $total->{'total_item'} = &$escape_function($payment->{'description'});
2324 $paymenttotal += $payment->{'amount'};
2325 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2326 $adjusttotal += $payment->{'amount'};
2327 if ( $multisection ) {
2328 my $money = $old_latex ? '' : $money_char;
2329 push @detail_items, {
2330 ext_description => [],
2333 description => &$escape_function($payment->{'description'}),
2334 amount => $money. $payment->{'amount'},
2336 section => $adjust_section,
2339 push @total_items, $total;
2341 push @buf, [ $payment->{'description'},
2342 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2345 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2347 if ( $multisection ) {
2348 $adjust_section->{'subtotal'} = $other_money_char.
2349 sprintf('%.2f', $adjusttotal);
2350 push @sections, $adjust_section;
2355 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2356 $total->{'total_amount'} =
2357 &$embolden_function(
2358 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2360 if ( $multisection ) {
2361 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2362 $total->{'total_amount'};
2364 push @total_items, $total;
2366 push @buf,['','-----------'];
2367 push @buf,[$self->balance_due_msg, $money_char.
2368 sprintf("%10.2f", $balance_due ) ];
2372 if ( $multisection ) {
2373 push @sections, @$late_sections
2379 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2380 /invoice_lines\((\d*)\)/;
2381 $invoice_lines += $1 || scalar(@buf);
2384 die "no invoice_lines() functions in template?"
2385 if ( $format eq 'template' && !$wasfunc );
2387 if ($format eq 'template') {
2389 if ( $invoice_lines ) {
2390 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2391 $invoice_data{'total_pages'}++
2392 if scalar(@buf) % $invoice_lines;
2395 #setup subroutine for the template
2396 sub FS::cust_bill::_template::invoice_lines {
2397 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2399 scalar(@FS::cust_bill::_template::buf)
2400 ? shift @FS::cust_bill::_template::buf
2409 push @collect, split("\n",
2410 $text_template->fill_in( HASH => \%invoice_data,
2411 PACKAGE => 'FS::cust_bill::_template'
2414 $FS::cust_bill::_template::page++;
2416 map "$_\n", @collect;
2418 warn "filling in template for invoice ". $self->invnum. "\n"
2420 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2423 $text_template->fill_in(HASH => \%invoice_data);
2427 =item print_ps [ TIME [ , TEMPLATE ] ]
2429 Returns an postscript invoice, as a scalar.
2431 TIME an optional value used to control the printing of overdue messages. The
2432 default is now. It isn't the date of the invoice; that's the `_date' field.
2433 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2434 L<Time::Local> and L<Date::Parse> for conversion functions.
2441 my ($file, $lfile) = $self->print_latex(@_);
2442 my $ps = generate_ps($file);
2448 =item print_pdf [ TIME [ , TEMPLATE ] ]
2450 Returns an PDF invoice, as a scalar.
2452 TIME an optional value used to control the printing of overdue messages. The
2453 default is now. It isn't the date of the invoice; that's the `_date' field.
2454 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2455 L<Time::Local> and L<Date::Parse> for conversion functions.
2462 my ($file, $lfile) = $self->print_latex(@_);
2463 my $pdf = generate_pdf($file);
2469 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2471 Returns an HTML invoice, as a scalar.
2473 TIME an optional value used to control the printing of overdue messages. The
2474 default is now. It isn't the date of the invoice; that's the `_date' field.
2475 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2476 L<Time::Local> and L<Date::Parse> for conversion functions.
2478 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2479 when emailing the invoice as part of a multipart/related MIME email.
2487 %params = %{ shift() };
2489 $params{'time'} = shift;
2490 $params{'template'} = shift;
2491 $params{'cid'} = shift;
2494 $params{'format'} = 'html';
2496 $self->print_generic( %params );
2499 # quick subroutine for print_latex
2501 # There are ten characters that LaTeX treats as special characters, which
2502 # means that they do not simply typeset themselves:
2503 # # $ % & ~ _ ^ \ { }
2505 # TeX ignores blanks following an escaped character; if you want a blank (as
2506 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2510 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2511 $value =~ s/([<>])/\$$1\$/g;
2515 #utility methods for print_*
2517 sub _translate_old_latex_format {
2518 warn "_translate_old_latex_format called\n"
2525 if ( $line =~ /^%%Detail\s*$/ ) {
2527 push @template, q![@--!,
2528 q! foreach my $_tr_line (@detail_items) {!,
2529 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2530 q! $_tr_line->{'description'} .= !,
2531 q! "\\tabularnewline\n~~".!,
2532 q! join( "\\tabularnewline\n~~",!,
2533 q! @{$_tr_line->{'ext_description'}}!,
2537 while ( ( my $line_item_line = shift )
2538 !~ /^%%EndDetail\s*$/ ) {
2539 $line_item_line =~ s/'/\\'/g; # nice LTS
2540 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2541 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2542 push @template, " \$OUT .= '$line_item_line';";
2545 push @template, '}',
2548 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2550 push @template, '[@--',
2551 ' foreach my $_tr_line (@total_items) {';
2553 while ( ( my $total_item_line = shift )
2554 !~ /^%%EndTotalDetails\s*$/ ) {
2555 $total_item_line =~ s/'/\\'/g; # nice LTS
2556 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2557 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2558 push @template, " \$OUT .= '$total_item_line';";
2561 push @template, '}',
2565 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2566 push @template, $line;
2572 warn "$_\n" foreach @template;
2581 #check for an invoice- specific override (eventually)
2583 #check for a customer- specific override
2584 return $self->cust_main->invoice_terms
2585 if $self->cust_main->invoice_terms;
2587 #use configured default
2588 $conf->config('invoice_default_terms') || '';
2594 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2595 $duedate = $self->_date() + ( $1 * 86400 );
2602 $self->due_date ? time2str(shift, $self->due_date) : '';
2605 sub balance_due_msg {
2607 my $msg = 'Balance Due';
2608 return $msg unless $self->terms;
2609 if ( $self->due_date ) {
2610 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2611 } elsif ( $self->terms ) {
2612 $msg .= ' - '. $self->terms;
2617 sub balance_due_date {
2620 if ( $conf->exists('invoice_default_terms')
2621 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2622 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2627 =item invnum_date_pretty
2629 Returns a string with the invoice number and date, for example:
2630 "Invoice #54 (3/20/2008)"
2634 sub invnum_date_pretty {
2636 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2641 Returns a string with the date, for example: "3/20/2008"
2647 time2str('%x', $self->_date);
2650 sub _items_sections {
2657 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2660 if ( $cust_bill_pkg->pkgnum > 0 ) {
2661 my $usage = $cust_bill_pkg->usage;
2663 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2664 my $desc = $display->section;
2665 my $type = $display->type;
2667 if ( $display->post_total ) {
2668 if (! $type || $type eq 'S') {
2669 $l{$desc} += $cust_bill_pkg->setup
2670 if ( $cust_bill_pkg->setup != 0 );
2674 $l{$desc} += $cust_bill_pkg->recur
2675 if ( $cust_bill_pkg->recur != 0 );
2678 if ($type && $type eq 'R') {
2679 $l{$desc} += $cust_bill_pkg->recur - $usage
2680 if ( $cust_bill_pkg->recur != 0 );
2683 if ($type && $type eq 'U') {
2684 $l{$desc} += $usage;
2688 if (! $type || $type eq 'S') {
2689 $s{$desc} += $cust_bill_pkg->setup
2690 if ( $cust_bill_pkg->setup != 0 );
2694 $s{$desc} += $cust_bill_pkg->recur
2695 if ( $cust_bill_pkg->recur != 0 );
2698 if ($type && $type eq 'R') {
2699 $s{$desc} += $cust_bill_pkg->recur - $usage
2700 if ( $cust_bill_pkg->recur != 0 );
2703 if ($type && $type eq 'U') {
2704 $s{$desc} += $usage;
2715 push @$late, map { { 'description' => $_,
2716 'subtotal' => $l{$_},
2720 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2727 #my @display = scalar(@_)
2729 # : qw( _items_previous _items_pkg );
2730 # #: qw( _items_pkg );
2731 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2732 my @display = qw( _items_previous _items_pkg );
2735 foreach my $display ( @display ) {
2736 push @b, $self->$display(@_);
2741 sub _items_previous {
2743 my $cust_main = $self->cust_main;
2744 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2746 foreach ( @pr_cust_bill ) {
2748 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2749 ' ('. time2str('%x',$_->_date). ')',
2750 #'pkgpart' => 'N/A',
2752 'amount' => sprintf("%.2f", $_->owed),
2758 # 'description' => 'Previous Balance',
2759 # #'pkgpart' => 'N/A',
2760 # 'pkgnum' => 'N/A',
2761 # 'amount' => sprintf("%10.2f", $pr_total ),
2762 # 'ext_description' => [ map {
2763 # "Invoice ". $_->invnum.
2764 # " (". time2str("%x",$_->_date). ") ".
2765 # sprintf("%10.2f", $_->owed)
2766 # } @pr_cust_bill ],
2773 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2774 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2778 return 0 unless $a cmp $b;
2779 return -1 if $b eq 'Tax';
2780 return 1 if $a eq 'Tax';
2781 return -1 if $b eq 'Other surcharges';
2782 return 1 if $a eq 'Other surcharges';
2788 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2789 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2792 sub _items_cust_bill_pkg {
2794 my $cust_bill_pkg = shift;
2797 my $format = $opt{format} || '';
2798 my $escape_function = $opt{escape_function} || sub { shift };
2799 my $format_function = $opt{format_function} || '';
2800 my $unsquelched = $opt{unsquelched} || '';
2801 my $section = $opt{section}->{description} if $opt{section};
2804 my ($s, $r, $u) = ( undef, undef, undef );
2805 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2808 foreach ( $s, $r, $u ) {
2809 if ( $_ && !$cust_bill_pkg->hidden ) {
2810 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2811 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2817 foreach my $display ( grep { defined($section)
2818 ? $_->section eq $section
2821 $cust_bill_pkg->cust_bill_pkg_display
2825 my $type = $display->type;
2827 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2829 my $desc = $cust_bill_pkg->desc;
2830 $desc = substr($desc, 0, 50). '...'
2831 if $format eq 'latex' && length($desc) > 50;
2833 my %details_opt = ( 'format' => $format,
2834 'escape_function' => $escape_function,
2835 'format_function' => $format_function,
2838 if ( $cust_bill_pkg->pkgnum > 0 ) {
2840 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2842 my $description = $desc;
2843 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2846 push @d, map &{$escape_function}($_),
2847 $cust_pkg->h_labels_short($self->_date)
2848 unless $cust_pkg->part_pkg->hide_svc_detail
2849 || $cust_bill_pkg->hidden;
2850 push @d, $cust_bill_pkg->details(%details_opt)
2851 if $cust_bill_pkg->recur == 0;
2853 if ( $cust_bill_pkg->hidden ) {
2854 $s->{amount} += $cust_bill_pkg->setup;
2855 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2856 push @{ $s->{ext_description} }, @d;
2859 description => $description,
2860 #pkgpart => $part_pkg->pkgpart,
2861 pkgnum => $cust_bill_pkg->pkgnum,
2862 amount => $cust_bill_pkg->setup,
2863 unit_amount => $cust_bill_pkg->unitsetup,
2864 quantity => $cust_bill_pkg->quantity,
2865 ext_description => \@d,
2871 if ( $cust_bill_pkg->recur != 0 &&
2872 ( !$type || $type eq 'R' || $type eq 'U' )
2876 my $is_summary = $display->summary;
2877 my $description = $is_summary ? "Usage charges" : $desc;
2879 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2880 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2881 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2886 #at least until cust_bill_pkg has "past" ranges in addition to
2887 #the "future" sdate/edate ones... see #3032
2888 my @dates = ( $self->_date );
2889 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2890 push @dates, $prev->sdate if $prev;
2892 push @d, map &{$escape_function}($_),
2893 $cust_pkg->h_labels_short(@dates)
2894 #$cust_bill_pkg->edate,
2895 #$cust_bill_pkg->sdate)
2896 unless $cust_pkg->part_pkg->hide_svc_detail
2897 || $cust_bill_pkg->itemdesc
2898 || $cust_bill_pkg->hidden
2901 push @d, $cust_bill_pkg->details(%details_opt)
2902 unless ($is_summary || $type && $type eq 'R');
2906 $amount = $cust_bill_pkg->recur;
2907 }elsif($type eq 'R') {
2908 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2909 }elsif($type eq 'U') {
2910 $amount = $cust_bill_pkg->usage;
2913 if ( !$type || $type eq 'R' ) {
2915 if ( $cust_bill_pkg->hidden ) {
2916 $r->{amount} += $amount;
2917 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
2918 push @{ $r->{ext_description} }, @d;
2921 description => $description,
2922 #pkgpart => $part_pkg->pkgpart,
2923 pkgnum => $cust_bill_pkg->pkgnum,
2925 unit_amount => $cust_bill_pkg->unitrecur,
2926 quantity => $cust_bill_pkg->quantity,
2927 ext_description => \@d,
2931 } elsif ( $amount ) { # && $type eq 'U'
2933 if ( $cust_bill_pkg->hidden ) {
2934 $u->{amount} += $amount;
2935 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
2936 push @{ $u->{ext_description} }, @d;
2939 description => $description,
2940 #pkgpart => $part_pkg->pkgpart,
2941 pkgnum => $cust_bill_pkg->pkgnum,
2943 unit_amount => $cust_bill_pkg->unitrecur,
2944 quantity => $cust_bill_pkg->quantity,
2945 ext_description => \@d,
2951 } # recurring or usage with recurring charge
2953 } else { #pkgnum tax or one-shot line item (??)
2955 if ( $cust_bill_pkg->setup != 0 ) {
2957 'description' => $desc,
2958 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2961 if ( $cust_bill_pkg->recur != 0 ) {
2963 'description' => "$desc (".
2964 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2965 time2str("%x", $cust_bill_pkg->edate). ')',
2966 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2976 foreach ( $s, $r, $u ) {
2978 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2979 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2988 sub _items_credits {
2989 my( $self, %opt ) = @_;
2990 my $trim_len = $opt{'trim_len'} || 60;
2994 foreach ( $self->cust_credited ) {
2996 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2998 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
2999 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3000 $reason = " ($reason) " if $reason;
3003 #'description' => 'Credit ref\#'. $_->crednum.
3004 # " (". time2str("%x",$_->cust_credit->_date) .")".
3006 'description' => 'Credit applied '.
3007 time2str("%x",$_->cust_credit->_date). $reason,
3008 'amount' => sprintf("%.2f",$_->amount),
3016 sub _items_payments {
3020 #get & print payments
3021 foreach ( $self->cust_bill_pay ) {
3023 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3026 'description' => "Payment received ".
3027 time2str("%x",$_->cust_pay->_date ),
3028 'amount' => sprintf("%.2f", $_->amount )
3043 =item process_reprint
3047 sub process_reprint {
3048 process_re_X('print', @_);
3051 =item process_reemail
3055 sub process_reemail {
3056 process_re_X('email', @_);
3064 process_re_X('fax', @_);
3072 process_re_X('ftp', @_);
3079 sub process_respool {
3080 process_re_X('spool', @_);
3083 use Storable qw(thaw);
3087 my( $method, $job ) = ( shift, shift );
3088 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3090 my $param = thaw(decode_base64(shift));
3091 warn Dumper($param) if $DEBUG;
3102 my($method, $job, %param ) = @_;
3104 warn "re_X $method for job $job with param:\n".
3105 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3108 #some false laziness w/search/cust_bill.html
3110 my $orderby = 'ORDER BY cust_bill._date';
3112 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3114 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3116 my @cust_bill = qsearch( {
3117 #'select' => "cust_bill.*",
3118 'table' => 'cust_bill',
3119 'addl_from' => $addl_from,
3121 'extra_sql' => $extra_sql,
3122 'order_by' => $orderby,
3126 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3128 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3131 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3132 foreach my $cust_bill ( @cust_bill ) {
3133 $cust_bill->$method();
3135 if ( $job ) { #progressbar foo
3137 if ( time - $min_sec > $last ) {
3138 my $error = $job->update_statustext(
3139 int( 100 * $num / scalar(@cust_bill) )
3141 die $error if $error;
3152 =head1 CLASS METHODS
3158 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3164 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3169 Returns an SQL fragment to retreive the net amount (charged minus credited).
3175 'charged - '. $class->credited_sql;
3180 Returns an SQL fragment to retreive the amount paid against this invoice.
3186 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3187 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3192 Returns an SQL fragment to retreive the amount credited against this invoice.
3198 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3199 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3202 =item search_sql HASHREF
3204 Class method which returns an SQL WHERE fragment to search for parameters
3205 specified in HASHREF. Valid parameters are
3211 Epoch date (UNIX timestamp) setting a lower bound for _date values
3215 Epoch date (UNIX timestamp) setting an upper bound for _date values
3229 =item newest_percust
3233 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3238 my($class, $param) = @_;
3240 warn "$me search_sql called with params: \n".
3241 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3246 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3247 push @search, "cust_bill._date >= $1";
3249 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3250 push @search, "cust_bill._date < $1";
3252 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3253 push @search, "cust_bill.invnum >= $1";
3255 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3256 push @search, "cust_bill.invnum <= $1";
3258 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3259 push @search, "cust_main.agentnum = $1";
3262 push @search, '0 != '. FS::cust_bill->owed_sql
3263 if $param->{'open'};
3265 push @search, '0 != '. FS::cust_bill->net_sql
3268 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3269 if $param->{'days'};
3271 if ( $param->{'newest_percust'} ) {
3273 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3274 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3276 my @newest_where = map { my $x = $_;
3277 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3280 grep ! /^cust_main./, @search;
3281 my $newest_where = scalar(@newest_where)
3282 ? ' AND '. join(' AND ', @newest_where)
3286 push @search, "cust_bill._date = (
3287 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3288 WHERE newest_cust_bill.custnum = cust_bill.custnum
3294 my $curuser = $FS::CurrentUser::CurrentUser;
3295 if ( $curuser->username eq 'fs_queue'
3296 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3298 my $newuser = qsearchs('access_user', {
3299 'username' => $username,
3303 $curuser = $newuser;
3305 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3309 push @search, $curuser->agentnums_sql;
3311 join(' AND ', @search );
3323 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3324 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base