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;
23 use FS::cust_credit_bill;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
29 use FS::cust_bill_pay;
30 use FS::cust_bill_pay_batch;
31 use FS::part_bill_event;
34 @ISA = qw( FS::cust_main_Mixin FS::Record );
37 $me = '[FS::cust_bill]';
39 #ask FS::UID to run this stuff for us later
40 FS::UID->install_callback( sub {
42 $money_char = $conf->config('money_char') || '$';
47 FS::cust_bill - Object methods for cust_bill records
53 $record = new FS::cust_bill \%hash;
54 $record = new FS::cust_bill { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
66 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
68 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
70 @cust_pay_objects = $cust_bill->cust_pay;
72 $tax_amount = $record->tax;
74 @lines = $cust_bill->print_text;
75 @lines = $cust_bill->print_text $time;
79 An FS::cust_bill object represents an invoice; a declaration that a customer
80 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
81 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
82 following fields are currently supported:
86 =item invnum - primary key (assigned automatically for new invoices)
88 =item custnum - customer (see L<FS::cust_main>)
90 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
91 L<Time::Local> and L<Date::Parse> for conversion functions.
93 =item charged - amount of this invoice
95 =item printed - deprecated
97 =item closed - books closed flag, empty or `Y'
107 Creates a new invoice. To add the invoice to the database, see L<"insert">.
108 Invoices are normally created by calling the bill method of a customer object
109 (see L<FS::cust_main>).
113 sub table { 'cust_bill'; }
115 sub cust_linked { $_[0]->cust_main_custnum; }
116 sub cust_unlinked_msg {
118 "WARNING: can't find cust_main.custnum ". $self->custnum.
119 ' (cust_bill.invnum '. $self->invnum. ')';
124 Adds this invoice to the database ("Posts" the invoice). If there is an error,
125 returns the error, otherwise returns false.
129 This method now works but you probably shouldn't use it. Instead, apply a
130 credit against the invoice.
132 Using this method to delete invoices outright is really, really bad. There
133 would be no record you ever posted this invoice, and there are no check to
134 make sure charged = 0 or that there are no associated cust_bill_pkg records.
136 Really, don't use it.
142 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
143 $self->SUPER::delete(@_);
146 =item replace OLD_RECORD
148 Replaces the OLD_RECORD with this one in the database. If there is an error,
149 returns the error, otherwise returns false.
151 Only printed may be changed. printed is normally updated by calling the
152 collect method of a customer object (see L<FS::cust_main>).
156 #replace can be inherited from Record.pm
158 # replace_check is now the preferred way to #implement replace data checks
159 # (so $object->replace() works without an argument)
162 my( $new, $old ) = ( shift, shift );
163 return "Can't change custnum!" unless $old->custnum == $new->custnum;
164 #return "Can't change _date!" unless $old->_date eq $new->_date;
165 return "Can't change _date!" unless $old->_date == $new->_date;
166 return "Can't change charged!" unless $old->charged == $new->charged
167 || $old->charged == 0;
174 Checks all fields to make sure this is a valid invoice. If there is an error,
175 returns the error, otherwise returns false. Called by the insert and replace
184 $self->ut_numbern('invnum')
185 || $self->ut_number('custnum')
186 || $self->ut_numbern('_date')
187 || $self->ut_money('charged')
188 || $self->ut_numbern('printed')
189 || $self->ut_enum('closed', [ '', 'Y' ])
191 return $error if $error;
193 return "Unknown customer"
194 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
196 $self->_date(time) unless $self->_date;
198 $self->printed(0) if $self->printed eq '';
205 Returns a list consisting of the total previous balance for this customer,
206 followed by the previous outstanding invoices (as FS::cust_bill objects also).
213 my @cust_bill = sort { $a->_date <=> $b->_date }
214 grep { $_->owed != 0 && $_->_date < $self->_date }
215 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
217 foreach ( @cust_bill ) { $total += $_->owed; }
223 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
229 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
234 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
241 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
243 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
246 =item open_cust_bill_pkg
248 Returns the open line items for this invoice.
250 Note that cust_bill_pkg with both setup and recur fees are returned as two
251 separate line items, each with only one fee.
255 # modeled after cust_main::open_cust_bill
256 sub open_cust_bill_pkg {
259 # grep { $_->owed > 0 } $self->cust_bill_pkg
261 my %other = ( 'recur' => 'setup',
262 'setup' => 'recur', );
264 foreach my $field ( qw( recur setup )) {
265 push @open, map { $_->set( $other{$field}, 0 ); $_; }
266 grep { $_->owed($field) > 0 }
267 $self->cust_bill_pkg;
273 =item cust_bill_event
275 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
279 sub cust_bill_event {
281 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
284 =item num_cust_bill_event
286 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
290 sub num_cust_bill_event {
293 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
294 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
295 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
296 $sth->fetchrow_arrayref->[0];
301 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
305 #false laziness w/cust_pkg.pm
309 'table' => 'cust_event',
310 'addl_from' => 'JOIN part_event USING ( eventpart )',
311 'hashref' => { 'tablenum' => $self->invnum },
312 'extra_sql' => " AND eventtable = 'cust_bill' ",
318 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
322 #false laziness w/cust_pkg.pm
326 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
327 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
328 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
329 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
330 $sth->fetchrow_arrayref->[0];
335 Returns the customer (see L<FS::cust_main>) for this invoice.
341 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
344 =item cust_suspend_if_balance_over AMOUNT
346 Suspends the customer associated with this invoice if the total amount owed on
347 this invoice and all older invoices is greater than the specified amount.
349 Returns a list: an empty list on success or a list of errors.
353 sub cust_suspend_if_balance_over {
354 my( $self, $amount ) = ( shift, shift );
355 my $cust_main = $self->cust_main;
356 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
359 $cust_main->suspend(@_);
365 Depreciated. See the cust_credited method.
367 #Returns a list consisting of the total previous credited (see
368 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
369 #outstanding credits (FS::cust_credit objects).
375 croak "FS::cust_bill->cust_credit depreciated; see ".
376 "FS::cust_bill->cust_credit_bill";
379 #my @cust_credit = sort { $a->_date <=> $b->_date }
380 # grep { $_->credited != 0 && $_->_date < $self->_date }
381 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
383 #foreach (@cust_credit) { $total += $_->credited; }
384 #$total, @cust_credit;
389 Depreciated. See the cust_bill_pay method.
391 #Returns all payments (see L<FS::cust_pay>) for this invoice.
397 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
399 #sort { $a->_date <=> $b->_date }
400 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
406 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
412 sort { $a->_date <=> $b->_date }
413 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
418 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
424 sort { $a->_date <=> $b->_date }
425 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
431 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
438 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
440 foreach (@taxlines) { $total += $_->setup; }
446 Returns the amount owed (still outstanding) on this invoice, which is charged
447 minus all payment applications (see L<FS::cust_bill_pay>) and credit
448 applications (see L<FS::cust_credit_bill>).
454 my $balance = $self->charged;
455 $balance -= $_->amount foreach ( $self->cust_bill_pay );
456 $balance -= $_->amount foreach ( $self->cust_credited );
457 $balance = sprintf( "%.2f", $balance);
458 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
462 =item apply_payments_and_credits
466 sub apply_payments_and_credits {
469 local $SIG{HUP} = 'IGNORE';
470 local $SIG{INT} = 'IGNORE';
471 local $SIG{QUIT} = 'IGNORE';
472 local $SIG{TERM} = 'IGNORE';
473 local $SIG{TSTP} = 'IGNORE';
474 local $SIG{PIPE} = 'IGNORE';
476 my $oldAutoCommit = $FS::UID::AutoCommit;
477 local $FS::UID::AutoCommit = 0;
480 $self->select_for_update; #mutex
482 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
483 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
485 while ( $self->owed > 0 and ( @payments || @credits ) ) {
488 if ( @payments && @credits ) {
490 #decide which goes first by weight of top (unapplied) line item
492 my @open_lineitems = $self->open_cust_bill_pkg;
495 max( map { $_->part_pkg->pay_weight || 0 }
500 my $max_credit_weight =
501 max( map { $_->part_pkg->credit_weight || 0 }
507 #if both are the same... payments first? it has to be something
508 if ( $max_pay_weight >= $max_credit_weight ) {
514 } elsif ( @payments ) {
516 } elsif ( @credits ) {
519 die "guru meditation #12 and 35";
522 if ( $app eq 'pay' ) {
524 my $payment = shift @payments;
526 $app = new FS::cust_bill_pay {
527 'paynum' => $payment->paynum,
528 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
531 } elsif ( $app eq 'credit' ) {
533 my $credit = shift @credits;
535 $app = new FS::cust_credit_bill {
536 'crednum' => $credit->crednum,
537 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
541 die "guru meditation #12 and 35";
544 $app->invnum( $self->invnum );
546 my $error = $app->insert;
548 $dbh->rollback if $oldAutoCommit;
549 return "Error inserting ". $app->table. " record: $error";
551 die $error if $error;
555 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
560 =item generate_email PARAMHASH
562 PARAMHASH can contain the following:
566 =item from => sender address, required
568 =item tempate => alternate template name, optional
570 =item print_text => text attachment arrayref, optional
572 =item subject => email subject, optional
576 Returns an argument list to be passed to L<FS::Misc::send_email>.
587 my $me = '[FS::cust_bill::generate_email]';
590 'from' => $args{'from'},
591 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
594 if (ref($args{'to'}) eq 'ARRAY') {
595 $return{'to'} = $args{'to'};
597 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
598 $self->cust_main->invoicing_list
602 if ( $conf->exists('invoice_html') ) {
604 warn "$me creating HTML/text multipart message"
607 $return{'nobody'} = 1;
609 my $alternative = build MIME::Entity
610 'Type' => 'multipart/alternative',
611 'Encoding' => '7bit',
612 'Disposition' => 'inline'
616 if ( $conf->exists('invoice_email_pdf')
617 and scalar($conf->config('invoice_email_pdf_note')) ) {
619 warn "$me using 'invoice_email_pdf_note' in multipart message"
621 $data = [ map { $_ . "\n" }
622 $conf->config('invoice_email_pdf_note')
627 warn "$me not using 'invoice_email_pdf_note' in multipart message"
629 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
630 $data = $args{'print_text'};
632 $data = [ $self->print_text('', $args{'template'}) ];
637 $alternative->attach(
638 'Type' => 'text/plain',
639 #'Encoding' => 'quoted-printable',
640 'Encoding' => '7bit',
642 'Disposition' => 'inline',
645 $args{'from'} =~ /\@([\w\.\-]+)/;
646 my $from = $1 || 'example.com';
647 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
649 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
651 if ( defined($args{'template'}) && length($args{'template'})
652 && -e "$path/logo_". $args{'template'}. ".png"
655 $file = "$path/logo_". $args{'template'}. ".png";
657 $file = "$path/logo.png";
660 my $image = build MIME::Entity
661 'Type' => 'image/png',
662 'Encoding' => 'base64',
664 'Filename' => 'logo.png',
665 'Content-ID' => "<$content_id>",
668 $alternative->attach(
669 'Type' => 'text/html',
670 'Encoding' => 'quoted-printable',
671 'Data' => [ '<html>',
674 ' '. encode_entities($return{'subject'}),
677 ' <body bgcolor="#e8e8e8">',
678 $self->print_html('', $args{'template'}, $content_id),
682 'Disposition' => 'inline',
683 #'Filename' => 'invoice.pdf',
686 if ( $conf->exists('invoice_email_pdf') ) {
691 # multipart/alternative
697 my $related = build MIME::Entity 'Type' => 'multipart/related',
698 'Encoding' => '7bit';
700 #false laziness w/Misc::send_email
701 $related->head->replace('Content-type',
703 '; boundary="'. $related->head->multipart_boundary. '"'.
704 '; type=multipart/alternative'
707 $related->add_part($alternative);
709 $related->add_part($image);
711 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
713 $return{'mimeparts'} = [ $related, $pdf ];
717 #no other attachment:
719 # multipart/alternative
724 $return{'content-type'} = 'multipart/related';
725 $return{'mimeparts'} = [ $alternative, $image ];
726 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
727 #$return{'disposition'} = 'inline';
733 if ( $conf->exists('invoice_email_pdf') ) {
734 warn "$me creating PDF attachment"
737 #mime parts arguments a la MIME::Entity->build().
738 $return{'mimeparts'} = [
739 { $self->mimebuild_pdf('', $args{'template'}) }
743 if ( $conf->exists('invoice_email_pdf')
744 and scalar($conf->config('invoice_email_pdf_note')) ) {
746 warn "$me using 'invoice_email_pdf_note'"
748 $return{'body'} = [ map { $_ . "\n" }
749 $conf->config('invoice_email_pdf_note')
754 warn "$me not using 'invoice_email_pdf_note'"
756 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
757 $return{'body'} = $args{'print_text'};
759 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
772 Returns a list suitable for passing to MIME::Entity->build(), representing
773 this invoice as PDF attachment.
780 'Type' => 'application/pdf',
781 'Encoding' => 'base64',
782 'Data' => [ $self->print_pdf(@_) ],
783 'Disposition' => 'attachment',
784 'Filename' => 'invoice.pdf',
788 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
790 Sends this invoice to the destinations configured for this customer: sends
791 email, prints and/or faxes. See L<FS::cust_main_invoice>.
793 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
795 AGENTNUM, if specified, means that this invoice will only be sent for customers
796 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
797 single agent) or an arrayref of agentnums.
799 INVOICE_FROM, if specified, overrides the default email invoice From: address.
801 AMOUNT, if specified, only sends the invoice if the total amount owed on this
802 invoice and all older invoices is greater than the specified amount.
809 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
810 or die "invalid invoice number: " . $opt{invnum};
812 my @args = ( $opt{template}, $opt{agentnum} );
813 push @args, $opt{invoice_from}
814 if exists($opt{invoice_from}) && $opt{invoice_from};
816 my $error = $self->send( @args );
817 die $error if $error;
823 my $template = scalar(@_) ? shift : '';
824 if ( scalar(@_) && $_[0] ) {
825 my $agentnums = ref($_[0]) ? shift : [ shift ];
826 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
832 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
834 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
837 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
839 my @invoicing_list = $self->cust_main->invoicing_list;
841 $self->email($template, $invoice_from)
842 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
844 $self->print($template)
845 if grep { $_ eq 'POST' } @invoicing_list; #postal
847 $self->fax($template)
848 if grep { $_ eq 'FAX' } @invoicing_list; #fax
854 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
858 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
860 INVOICE_FROM, if specified, overrides the default email invoice From: address.
864 sub queueable_email {
867 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
868 or die "invalid invoice number: " . $opt{invnum};
870 my @args = ( $opt{template} );
871 push @args, $opt{invoice_from}
872 if exists($opt{invoice_from}) && $opt{invoice_from};
874 my $error = $self->email( @args );
875 die $error if $error;
881 my $template = scalar(@_) ? shift : '';
885 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
887 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
888 $self->cust_main->invoicing_list;
890 #better to notify this person than silence
891 @invoicing_list = ($invoice_from) unless @invoicing_list;
893 my $error = send_email(
894 $self->generate_email(
895 'from' => $invoice_from,
896 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
897 'template' => $template,
900 die "can't email invoice: $error\n" if $error;
901 #die "$error\n" if $error;
905 =item lpr_data [ TEMPLATENAME ]
907 Returns the postscript or plaintext for this invoice as an arrayref.
909 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
914 my( $self, $template) = @_;
915 $conf->exists('invoice_latex')
916 ? [ $self->print_ps('', $template) ]
917 : [ $self->print_text('', $template) ];
920 =item print [ TEMPLATENAME ]
924 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
930 my $template = scalar(@_) ? shift : '';
932 do_print $self->lpr_data($template);
935 =item fax [ TEMPLATENAME ]
939 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
945 my $template = scalar(@_) ? shift : '';
947 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
948 unless $conf->exists('invoice_latex');
950 my $dialstring = $self->cust_main->getfield('fax');
953 my $error = send_fax( 'docdata' => $self->lpr_data($template),
954 'dialstring' => $dialstring,
956 die $error if $error;
960 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
962 Like B<send>, but only sends the invoice if it is the newest open invoice for
972 grep { $_->owed > 0 }
973 qsearch('cust_bill', {
974 'custnum' => $self->custnum,
975 #'_date' => { op=>'>', value=>$self->_date },
976 'invnum' => { op=>'>', value=>$self->invnum },
983 =item send_csv OPTION => VALUE, ...
985 Sends invoice as a CSV data-file to a remote host with the specified protocol.
989 protocol - currently only "ftp"
995 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
996 and YYMMDDHHMMSS is a timestamp.
998 See L</print_csv> for a description of the output format.
1003 my($self, %opt) = @_;
1007 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1008 mkdir $spooldir, 0700 unless -d $spooldir;
1010 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1011 my $file = "$spooldir/$tracctnum.csv";
1013 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1015 open(CSV, ">$file") or die "can't open $file: $!";
1023 if ( $opt{protocol} eq 'ftp' ) {
1024 eval "use Net::FTP;";
1026 $net = Net::FTP->new($opt{server}) or die @$;
1028 die "unknown protocol: $opt{protocol}";
1031 $net->login( $opt{username}, $opt{password} )
1032 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1034 $net->binary or die "can't set binary mode";
1036 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1038 $net->put($file) or die "can't put $file: $!";
1048 Spools CSV invoice data.
1054 =item format - 'default' or 'billco'
1056 =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>).
1058 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1060 =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.
1067 my($self, %opt) = @_;
1069 my $cust_main = $self->cust_main;
1071 if ( $opt{'dest'} ) {
1072 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1073 $cust_main->invoicing_list;
1074 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1075 || ! keys %invoicing_list;
1078 if ( $opt{'balanceover'} ) {
1080 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1083 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1084 mkdir $spooldir, 0700 unless -d $spooldir;
1086 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1090 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1091 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1094 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1096 open(CSV, ">>$file") or die "can't open $file: $!";
1097 flock(CSV, LOCK_EX);
1102 if ( lc($opt{'format'}) eq 'billco' ) {
1104 flock(CSV, LOCK_UN);
1109 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1112 open(CSV,">>$file") or die "can't open $file: $!";
1113 flock(CSV, LOCK_EX);
1119 flock(CSV, LOCK_UN);
1126 =item print_csv OPTION => VALUE, ...
1128 Returns CSV data for this invoice.
1132 format - 'default' or 'billco'
1134 Returns a list consisting of two scalars. The first is a single line of CSV
1135 header information for this invoice. The second is one or more lines of CSV
1136 detail information for this invoice.
1138 If I<format> is not specified or "default", the fields of the CSV file are as
1141 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1145 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1147 B<record_type> is C<cust_bill> for the initial header line only. The
1148 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1149 fields are filled in.
1151 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1152 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1155 =item invnum - invoice number
1157 =item custnum - customer number
1159 =item _date - invoice date
1161 =item charged - total invoice amount
1163 =item first - customer first name
1165 =item last - customer first name
1167 =item company - company name
1169 =item address1 - address line 1
1171 =item address2 - address line 1
1181 =item pkg - line item description
1183 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1185 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1187 =item sdate - start date for recurring fee
1189 =item edate - end date for recurring fee
1193 If I<format> is "billco", the fields of the header CSV file are as follows:
1195 +-------------------------------------------------------------------+
1196 | FORMAT HEADER FILE |
1197 |-------------------------------------------------------------------|
1198 | Field | Description | Name | Type | Width |
1199 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1200 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1201 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1202 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1203 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1204 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1205 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1206 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1207 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1208 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1209 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1210 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1211 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1212 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1213 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1214 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1215 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1216 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1217 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1218 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1219 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1220 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1221 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1222 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1223 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1224 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1225 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1226 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1227 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1228 +-------+-------------------------------+------------+------+-------+
1230 If I<format> is "billco", the fields of the detail CSV file are as follows:
1232 FORMAT FOR DETAIL FILE
1234 Field | Description | Name | Type | Width
1235 1 | N/A-Leave Empty | RC | CHAR | 2
1236 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1237 3 | Account Number | TRACCTNUM | CHAR | 15
1238 4 | Invoice Number | TRINVOICE | CHAR | 15
1239 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1240 6 | Transaction Detail | DETAILS | CHAR | 100
1241 7 | Amount | AMT | NUM* | 9
1242 8 | Line Format Control** | LNCTRL | CHAR | 2
1243 9 | Grouping Code | GROUP | CHAR | 2
1244 10 | User Defined | ACCT CODE | CHAR | 15
1249 my($self, %opt) = @_;
1251 eval "use Text::CSV_XS";
1254 my $cust_main = $self->cust_main;
1256 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1258 if ( lc($opt{'format'}) eq 'billco' ) {
1261 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1263 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1265 my( $previous_balance, @unused ) = $self->previous; #previous balance
1267 my $pmt_cr_applied = 0;
1268 $pmt_cr_applied += $_->{'amount'}
1269 foreach ( $self->_items_payments, $self->_items_credits ) ;
1271 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1274 '', # 1 | N/A-Leave Empty CHAR 2
1275 '', # 2 | N/A-Leave Empty CHAR 15
1276 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1277 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1278 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1279 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1280 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1281 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1282 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1283 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1284 '', # 10 | Ancillary Billing Information CHAR 30
1285 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1286 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1289 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1292 $duedate, # 14 | Bill Due Date CHAR 10
1294 $previous_balance, # 15 | Previous Balance NUM* 9
1295 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1296 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1297 $totaldue, # 18 | Total Amt Due NUM* 9
1298 $totaldue, # 19 | Total Amt Due NUM* 9
1299 '', # 20 | 30 Day Aging NUM* 9
1300 '', # 21 | 60 Day Aging NUM* 9
1301 '', # 22 | 90 Day Aging NUM* 9
1302 'N', # 23 | Y/N CHAR 1
1303 '', # 24 | Remittance automation CHAR 100
1304 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1305 $self->custnum, # 26 | Customer Reference Number CHAR 15
1306 '0', # 27 | Federal Tax*** NUM* 9
1307 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1308 '0', # 29 | Other Taxes & Fees*** NUM* 9
1317 time2str("%x", $self->_date),
1318 sprintf("%.2f", $self->charged),
1319 ( map { $cust_main->getfield($_) }
1320 qw( first last company address1 address2 city state zip country ) ),
1322 ) or die "can't create csv";
1325 my $header = $csv->string. "\n";
1328 if ( lc($opt{'format'}) eq 'billco' ) {
1331 foreach my $item ( $self->_items_pkg ) {
1334 '', # 1 | N/A-Leave Empty CHAR 2
1335 '', # 2 | N/A-Leave Empty CHAR 15
1336 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1337 $self->invnum, # 4 | Invoice Number CHAR 15
1338 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1339 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1340 $item->{'amount'}, # 7 | Amount NUM* 9
1341 '', # 8 | Line Format Control** CHAR 2
1342 '', # 9 | Grouping Code CHAR 2
1343 '', # 10 | User Defined CHAR 15
1346 $detail .= $csv->string. "\n";
1352 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1354 my($pkg, $setup, $recur, $sdate, $edate);
1355 if ( $cust_bill_pkg->pkgnum ) {
1357 ($pkg, $setup, $recur, $sdate, $edate) = (
1358 $cust_bill_pkg->part_pkg->pkg,
1359 ( $cust_bill_pkg->setup != 0
1360 ? sprintf("%.2f", $cust_bill_pkg->setup )
1362 ( $cust_bill_pkg->recur != 0
1363 ? sprintf("%.2f", $cust_bill_pkg->recur )
1365 ( $cust_bill_pkg->sdate
1366 ? time2str("%x", $cust_bill_pkg->sdate)
1368 ($cust_bill_pkg->edate
1369 ?time2str("%x", $cust_bill_pkg->edate)
1373 } else { #pkgnum tax
1374 next unless $cust_bill_pkg->setup != 0;
1375 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1376 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1378 ($pkg, $setup, $recur, $sdate, $edate) =
1379 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1385 ( map { '' } (1..11) ),
1386 ($pkg, $setup, $recur, $sdate, $edate)
1387 ) or die "can't create csv";
1389 $detail .= $csv->string. "\n";
1395 ( $header, $detail );
1401 Pays this invoice with a compliemntary payment. If there is an error,
1402 returns the error, otherwise returns false.
1408 my $cust_pay = new FS::cust_pay ( {
1409 'invnum' => $self->invnum,
1410 'paid' => $self->owed,
1413 'payinfo' => $self->cust_main->payinfo,
1421 Attempts to pay this invoice with a credit card payment via a
1422 Business::OnlinePayment realtime gateway. See
1423 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1424 for supported processors.
1430 $self->realtime_bop( 'CC', @_ );
1435 Attempts to pay this invoice with an electronic check (ACH) payment via a
1436 Business::OnlinePayment realtime gateway. See
1437 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1438 for supported processors.
1444 $self->realtime_bop( 'ECHECK', @_ );
1449 Attempts to pay this invoice with phone bill (LEC) payment via a
1450 Business::OnlinePayment realtime gateway. See
1451 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1452 for supported processors.
1458 $self->realtime_bop( 'LEC', @_ );
1462 my( $self, $method ) = @_;
1464 my $cust_main = $self->cust_main;
1465 my $balance = $cust_main->balance;
1466 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1467 $amount = sprintf("%.2f", $amount);
1468 return "not run (balance $balance)" unless $amount > 0;
1470 my $description = 'Internet Services';
1471 if ( $conf->exists('business-onlinepayment-description') ) {
1472 my $dtempl = $conf->config('business-onlinepayment-description');
1474 my $agent_obj = $cust_main->agent
1475 or die "can't retreive agent for $cust_main (agentnum ".
1476 $cust_main->agentnum. ")";
1477 my $agent = $agent_obj->agent;
1478 my $pkgs = join(', ',
1479 map { $_->part_pkg->pkg }
1480 grep { $_->pkgnum } $self->cust_bill_pkg
1482 $description = eval qq("$dtempl");
1485 $cust_main->realtime_bop($method, $amount,
1486 'description' => $description,
1487 'invnum' => $self->invnum,
1492 =item batch_card OPTION => VALUE...
1494 Adds a payment for this invoice to the pending credit card batch (see
1495 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1496 runs the payment using a realtime gateway.
1501 my ($self, %options) = @_;
1502 my $cust_main = $self->cust_main;
1504 $options{invnum} = $self->invnum;
1506 $cust_main->batch_card(%options);
1509 sub _agent_template {
1511 $self->cust_main->agent_template;
1514 sub _agent_invoice_from {
1516 $self->cust_main->agent_invoice_from;
1519 =item print_text [ TIME [ , TEMPLATE ] ]
1521 Returns an text invoice, as a list of lines.
1523 TIME an optional value used to control the printing of overdue messages. The
1524 default is now. It isn't the date of the invoice; that's the `_date' field.
1525 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1526 L<Time::Local> and L<Date::Parse> for conversion functions.
1531 my( $self, $today, $template ) = @_;
1533 my %params = ( 'format' => 'template' );
1534 $params{'time'} = $today if $today;
1535 $params{'template'} = $template if $template;
1537 $self->print_generic( %params );
1540 =item print_latex [ TIME [ , TEMPLATE ] ]
1542 Internal method - returns a filename of a filled-in LaTeX template for this
1543 invoice (Note: add ".tex" to get the actual filename), and a filename of
1544 an associated logo (with the .eps extension included).
1546 See print_ps and print_pdf for methods that return PostScript and PDF output.
1548 TIME an optional value used to control the printing of overdue messages. The
1549 default is now. It isn't the date of the invoice; that's the `_date' field.
1550 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1551 L<Time::Local> and L<Date::Parse> for conversion functions.
1557 my( $self, $today, $template ) = @_;
1559 my %params = ( 'format' => 'latex' );
1560 $params{'time'} = $today if $today;
1561 $params{'template'} = $template if $template;
1563 $template ||= $self->_agent_template;
1565 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1566 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1570 ) or die "can't open temp file: $!\n";
1572 if ($template && $conf->exists("logo_${template}.eps")) {
1573 print $lh $conf->config_binary("logo_${template}.eps")
1574 or die "can't write temp file: $!\n";
1576 print $lh $conf->config_binary('logo.eps')
1577 or die "can't write temp file: $!\n";
1580 $params{'logo_file'} = $lh->filename;
1582 my @filled_in = $self->print_generic( %params );
1584 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1588 ) or die "can't open temp file: $!\n";
1589 print $fh join('', @filled_in );
1592 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1593 return ($1, $params{'logo_file'});
1597 =item print_generic OPTIONS_HASH
1599 Internal method - returns a filled-in template for this invoice as a scalar.
1601 See print_ps and print_pdf for methods that return PostScript and PDF output.
1603 Non optional options include
1604 format - latex, html, template
1606 Optional options include
1608 template - a value used as a suffix for a configuration template
1610 time - a value used to control the printing of overdue messages. The
1611 default is now. It isn't the date of the invoice; that's the `_date' field.
1612 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1613 L<Time::Local> and L<Date::Parse> for conversion functions.
1621 my( $self, %params ) = @_;
1622 my $today = $params{today} ? $params{today} : time;
1623 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1626 my $format = $params{format};
1627 die "Unknown format: $format"
1628 unless $format =~ /^(latex|html|template)$/;
1630 my $cust_main = $self->cust_main;
1631 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1632 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1635 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1636 'html' => [ '<%=', '%>' ],
1637 'template' => [ '{', '}' ],
1640 #create the template
1641 my $template = $params{template} ? $params{template} : $self->_agent_template;
1642 my $templatefile = "invoice_$format";
1643 $templatefile .= "_$template"
1644 if length($template);
1645 my @invoice_template = map "$_\n", $conf->config($templatefile)
1646 or die "cannot load config data $templatefile";
1649 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1650 #change this to a die when the old code is removed
1651 warn "old-style invoice template $templatefile; ".
1652 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1653 $old_latex = 'true';
1654 @invoice_template = _translate_old_latex_format(@invoice_template);
1657 my $text_template = new Text::Template(
1659 SOURCE => \@invoice_template,
1660 DELIMITERS => $delimiters{$format},
1663 $text_template->compile()
1664 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1667 # additional substitution could possibly cause breakage in existing templates
1668 my %convert_maps = (
1670 'notes' => sub { map "$_", @_ },
1671 'footer' => sub { map "$_", @_ },
1672 'smallfooter' => sub { map "$_", @_ },
1673 'returnaddress' => sub { map "$_", @_ },
1674 'coupon' => sub { map "$_", @_ },
1680 s/%%(.*)$/<!-- $1 -->/g;
1681 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1682 s/\\begin\{enumerate\}/<ol>/g;
1684 s/\\end\{enumerate\}/<\/ol>/g;
1685 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1694 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1696 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1701 s/\\\\\*?\s*$/<BR>/;
1702 s/\\hyphenation\{[\w\s\-]+}//;
1706 'coupon' => sub { "" },
1713 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1714 s/\\begin\{enumerate\}//g;
1716 s/\\end\{enumerate\}//g;
1717 s/\\textbf\{(.*)\}/$1/g;
1724 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1726 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1731 s/\\\\\*?\s*$/\n/; # dubious
1732 s/\\hyphenation\{[\w\s\-]+}//;
1736 'coupon' => sub { "" },
1741 # hashes for differing output formats
1742 my %nbsps = ( 'latex' => '~',
1743 'html' => '', # '&nbps;' would be nice
1744 'template' => '', # not used
1746 my $nbsp = $nbsps{$format};
1748 my %escape_functions = ( 'latex' => \&_latex_escape,
1749 'html' => \&encode_entities,
1750 'template' => sub { shift },
1752 my $escape_function = $escape_functions{$format};
1754 my %date_formats = ( 'latex' => '%b %o, %Y',
1755 'html' => '%b %o, %Y',
1758 my $date_format = $date_formats{$format};
1760 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1762 'html' => sub { return '<b>'. shift(). '</b>'
1764 'template' => sub { shift },
1766 my $embolden_function = $embolden_functions{$format};
1769 # generate template variables
1772 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1776 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1782 $returnaddress = join("\n",
1783 $conf->config_orbase("invoice_${format}returnaddress", $template)
1786 } elsif ( grep /\S/,
1787 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1789 my $convert_map = $convert_maps{$format}{'returnaddress'};
1792 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1797 } elsif ( grep /\S/, $conf->config('company_address') ) {
1799 my $convert_map = $convert_maps{$format}{'returnaddress'};
1800 $returnaddress = join( "\n", &$convert_map(
1801 map { s/( {2,})/'~' x length($1)/eg;
1805 ( $conf->config('company_name'),
1806 $conf->config('company_address'),
1813 my $warning = "Couldn't find a return address; ".
1814 "do you need to set the company_address configuration value?";
1816 $returnaddress = $nbsp;
1817 #$returnaddress = $warning;
1821 my %invoice_data = (
1822 'company_name' => scalar( $conf->config('company_name') ),
1823 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1824 'custnum' => $self->custnum,
1825 'invnum' => $self->invnum,
1826 'date' => time2str($date_format, $self->_date),
1827 'today' => time2str('%b %o, %Y', $today),
1828 'agent' => &$escape_function($cust_main->agent->agent),
1829 'payname' => &$escape_function($cust_main->payname),
1830 'company' => &$escape_function($cust_main->company),
1831 'address1' => &$escape_function($cust_main->address1),
1832 'address2' => &$escape_function($cust_main->address2),
1833 'city' => &$escape_function($cust_main->city),
1834 'state' => &$escape_function($cust_main->state),
1835 'zip' => &$escape_function($cust_main->zip),
1836 'returnaddress' => $returnaddress,
1838 'terms' => $self->terms,
1839 'template' => $params{'template'},
1840 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1841 # better hang on to conf_dir for a while
1842 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1847 $invoice_data{'cid'} = $params{'cid'}
1850 my $countrydefault = $conf->config('countrydefault') || 'US';
1851 if ( $cust_main->country eq $countrydefault ) {
1852 $invoice_data{'country'} = '';
1854 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1858 $invoice_data{'address'} = \@address;
1860 $cust_main->payname.
1861 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1862 ? " (P.O. #". $cust_main->payinfo. ")"
1866 push @address, $cust_main->company
1867 if $cust_main->company;
1868 push @address, $cust_main->address1;
1869 push @address, $cust_main->address2
1870 if $cust_main->address2;
1872 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1873 push @address, $invoice_data{'country'}
1874 if $invoice_data{'country'};
1876 while (scalar(@address) < 5);
1878 $invoice_data{'logo_file'} = $params{'logo_file'}
1879 if $params{'logo_file'};
1881 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1882 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1883 #my $balance_due = $self->owed + $pr_total - $cr_total;
1884 my $balance_due = $self->owed + $pr_total;
1885 $invoice_data{'balance'} = $balance_due;
1887 #do variable substitution in notes, footer, smallfooter
1888 foreach my $include (qw( notes footer smallfooter coupon )) {
1890 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1893 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1895 @inc_src = $conf->config($inc_file);
1899 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1901 my $convert_map = $convert_maps{$format}{$include};
1903 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1904 s/--\@\]/$delimiters{$format}[1]/g;
1907 &$convert_map( $conf->config($inc_file) );
1911 my $inc_tt = new Text::Template (
1913 SOURCE => [ map "$_\n", @inc_src ],
1914 DELIMITERS => $delimiters{$format},
1915 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1917 unless ( $inc_tt->compile() ) {
1918 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1919 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1923 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1925 $invoice_data{$include} =~ s/\n+$//
1926 if ($format eq 'latex');
1929 $invoice_data{'po_line'} =
1930 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1931 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1934 my %money_chars = ( 'latex' => '',
1935 'html' => $conf->config('money_char') || '$',
1938 my $money_char = $money_chars{$format};
1940 my %other_money_chars = ( 'latex' => '\dollar ',
1941 'html' => $conf->config('money_char') || '$',
1944 my $other_money_char = $other_money_chars{$format};
1946 my @detail_items = ();
1947 my @total_items = ();
1951 $invoice_data{'detail_items'} = \@detail_items;
1952 $invoice_data{'total_items'} = \@total_items;
1953 $invoice_data{'buf'} = \@buf;
1954 $invoice_data{'sections'} = \@sections;
1956 my $previous_section = { 'description' => 'Previous Charges',
1957 'subtotal' => $other_money_char.
1958 sprintf('%.2f', $pr_total),
1962 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1963 'subtotal' => $taxtotal }; # adjusted below
1965 my $adjusttotal = 0;
1966 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
1967 'subtotal' => 0 }; # adjusted below
1969 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
1970 if ( $multisection ) {
1971 push @sections, $self->_items_sections;
1973 push @sections, { 'description' => '', 'subtotal' => '' };
1976 foreach my $line_item ( $conf->exists('disable_previous_balance')
1978 : $self->_items_previous
1982 ext_description => [],
1984 $detail->{'ref'} = $line_item->{'pkgnum'};
1985 $detail->{'quantity'} = 1;
1986 $detail->{'section'} = $previous_section;
1987 $detail->{'description'} = &$escape_function($line_item->{'description'});
1988 if ( exists $line_item->{'ext_description'} ) {
1989 @{$detail->{'ext_description'}} = map {
1990 &$escape_function($_);
1991 } @{$line_item->{'ext_description'}};
1993 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
1994 $line_item->{'amount'};
1995 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1997 push @detail_items, $detail;
1998 push @buf, [ $detail->{'description'},
1999 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2003 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2004 push @buf, ['','-----------'];
2005 push @buf, [ 'Total Previous Balance',
2006 $money_char. sprintf("%10.2f", $pr_total) ];
2010 foreach my $section (@sections) {
2012 $section->{'subtotal'} = $other_money_char.
2013 sprintf('%.2f', $section->{'subtotal'})
2016 if ( $section->{'description'} ) {
2017 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2023 $options{'section'} = $section if $multisection;
2024 $options{'format'} = $format;
2025 $options{'escape_function'} = $escape_function;
2027 foreach my $line_item ( $self->_items_pkg(%options) ) {
2029 ext_description => [],
2031 $detail->{'ref'} = $line_item->{'pkgnum'};
2032 $detail->{'quantity'} = $line_item->{'quantity'};
2033 $detail->{'section'} = $section;
2034 $detail->{'description'} = &$escape_function($line_item->{'description'});
2035 if ( exists $line_item->{'ext_description'} ) {
2036 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2038 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2039 $line_item->{'amount'};
2040 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2041 $line_item->{'unit_amount'};
2042 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2044 push @detail_items, $detail;
2045 push @buf, ( [ $detail->{'description'},
2046 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2048 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2052 if ( $section->{'description'} ) {
2053 push @buf, ( ['','-----------'],
2054 [ $section->{'description'}. ' sub-total',
2055 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2064 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2065 unshift @sections, $previous_section if $pr_total;
2068 foreach my $tax ( $self->_items_tax ) {
2070 $total->{'total_item'} = &$escape_function($tax->{'description'});
2071 $taxtotal += $tax->{'amount'};
2072 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2073 if ( $multisection ) {
2074 my $money = $old_latex ? '' : $money_char;
2075 push @detail_items, {
2076 ext_description => [],
2079 description => &$escape_function($tax->{'description'}),
2080 amount => $money. $tax->{'amount'},
2082 section => $tax_section,
2085 push @total_items, $total;
2087 push @buf,[ $total->{'total_item'},
2088 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2095 $total->{'total_item'} = 'Sub-total';
2096 $total->{'total_amount'} =
2097 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2099 if ( $multisection ) {
2100 $tax_section->{'subtotal'} = $other_money_char.
2101 sprintf('%.2f', $taxtotal);
2102 $tax_section->{'pretotal'} = 'New charges sub-total '.
2103 $total->{'total_amount'};
2104 push @sections, $tax_section if $taxtotal;
2106 unshift @total_items, $total;
2110 push @buf,['','-----------'];
2111 push @buf,[( $conf->exists('disable_previous_balance')
2113 : 'Total New Charges'
2115 $money_char. sprintf("%10.2f",$self->charged) ];
2120 $total->{'total_item'} = &$embolden_function('Total');
2121 $total->{'total_amount'} =
2122 &$embolden_function(
2125 $self->charged + ( $conf->exists('disable_previous_balance')
2131 if ( $multisection ) {
2132 $adjust_section->{'pretotal'} = 'New charges total '.
2133 $total->{'total_amount'};
2135 push @total_items, $total;
2137 push @buf,['','-----------'];
2138 push @buf,['Total Charges',
2140 sprintf( '%10.2f', $self->charged +
2141 ( $conf->exists('disable_previous_balance')
2150 unless ( $conf->exists('disable_previous_balance') ) {
2151 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2154 foreach my $credit ( $self->_items_credits ) {
2156 $total->{'total_item'} = &$escape_function($credit->{'description'});
2158 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2159 $adjusttotal += $credit->{'amount'};
2160 if ( $multisection ) {
2161 my $money = $old_latex ? '' : $money_char;
2162 push @detail_items, {
2163 ext_description => [],
2166 description => &$escape_function($credit->{'description'}),
2167 amount => $money. $credit->{'amount'},
2169 section => $adjust_section,
2172 push @total_items, $total;
2177 foreach ( $self->cust_credited ) {
2179 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2181 my $reason = substr($_->cust_credit->reason,0,32);
2182 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2183 $reason = " ($reason) " if $reason;
2185 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2186 $money_char. sprintf("%10.2f",$_->amount)
2191 foreach my $payment ( $self->_items_payments ) {
2193 $total->{'total_item'} = &$escape_function($payment->{'description'});
2195 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2196 $adjusttotal += $payment->{'amount'};
2197 if ( $multisection ) {
2198 my $money = $old_latex ? '' : $money_char;
2199 push @detail_items, {
2200 ext_description => [],
2203 description => &$escape_function($payment->{'description'}),
2204 amount => $money. $payment->{'amount'},
2206 section => $adjust_section,
2209 push @total_items, $total;
2211 push @buf, [ $payment->{'description'},
2212 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2216 if ( $multisection ) {
2217 $adjust_section->{'subtotal'} = $other_money_char.
2218 sprintf('%.2f', $adjusttotal);
2219 push @sections, $adjust_section;
2224 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2225 $total->{'total_amount'} =
2226 &$embolden_function(
2227 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2229 if ( $multisection ) {
2230 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2231 $total->{'total_amount'};
2233 push @total_items, $total;
2235 push @buf,['','-----------'];
2236 push @buf,[$self->balance_due_msg, $money_char.
2237 sprintf("%10.2f", $balance_due ) ];
2243 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2244 /invoice_lines\((\d*)\)/;
2245 $invoice_lines += $1 || scalar(@buf);
2248 die "no invoice_lines() functions in template?"
2249 if ( $format eq 'template' && !$wasfunc );
2251 if ($format eq 'template') {
2253 if ( $invoice_lines ) {
2254 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2255 $invoice_data{'total_pages'}++
2256 if scalar(@buf) % $invoice_lines;
2259 #setup subroutine for the template
2260 sub FS::cust_bill::_template::invoice_lines {
2261 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2263 scalar(@FS::cust_bill::_template::buf)
2264 ? shift @FS::cust_bill::_template::buf
2273 push @collect, split("\n",
2274 $text_template->fill_in( HASH => \%invoice_data,
2275 PACKAGE => 'FS::cust_bill::_template'
2278 $FS::cust_bill::_template::page++;
2280 map "$_\n", @collect;
2282 warn "filling in template for invoice ". $self->invnum. "\n"
2284 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2287 $text_template->fill_in(HASH => \%invoice_data);
2291 =item print_ps [ TIME [ , TEMPLATE ] ]
2293 Returns an postscript invoice, as a scalar.
2295 TIME an optional value used to control the printing of overdue messages. The
2296 default is now. It isn't the date of the invoice; that's the `_date' field.
2297 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2298 L<Time::Local> and L<Date::Parse> for conversion functions.
2305 my ($file, $lfile) = $self->print_latex(@_);
2306 my $ps = generate_ps($file);
2312 =item print_pdf [ TIME [ , TEMPLATE ] ]
2314 Returns an PDF invoice, as a scalar.
2316 TIME an optional value used to control the printing of overdue messages. The
2317 default is now. It isn't the date of the invoice; that's the `_date' field.
2318 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2319 L<Time::Local> and L<Date::Parse> for conversion functions.
2326 my ($file, $lfile) = $self->print_latex(@_);
2327 my $pdf = generate_pdf($file);
2333 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2335 Returns an HTML invoice, as a scalar.
2337 TIME an optional value used to control the printing of overdue messages. The
2338 default is now. It isn't the date of the invoice; that's the `_date' field.
2339 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2340 L<Time::Local> and L<Date::Parse> for conversion functions.
2342 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2343 when emailing the invoice as part of a multipart/related MIME email.
2348 my( $self, $today, $template, $cid ) = @_;
2350 my %params = ( 'format' => 'html' );
2351 $params{'time'} = $today if $today;
2352 $params{'template'} = $template if $template;
2353 $params{'cid'} = $cid if $cid;
2355 $self->print_generic( %params );
2358 # quick subroutine for print_latex
2360 # There are ten characters that LaTeX treats as special characters, which
2361 # means that they do not simply typeset themselves:
2362 # # $ % & ~ _ ^ \ { }
2364 # TeX ignores blanks following an escaped character; if you want a blank (as
2365 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2369 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2370 $value =~ s/([<>])/\$$1\$/g;
2374 #utility methods for print_*
2376 sub _translate_old_latex_format {
2377 warn "_translate_old_latex_format called\n"
2384 if ( $line =~ /^%%Detail\s*$/ ) {
2386 push @template, q![@--!,
2387 q! foreach my $_tr_line (@detail_items) {!,
2388 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2389 q! $_tr_line->{'description'} .= !,
2390 q! "\\tabularnewline\n~~".!,
2391 q! join( "\\tabularnewline\n~~",!,
2392 q! @{$_tr_line->{'ext_description'}}!,
2396 while ( ( my $line_item_line = shift )
2397 !~ /^%%EndDetail\s*$/ ) {
2398 $line_item_line =~ s/'/\\'/g; # nice LTS
2399 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2400 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2401 push @template, " \$OUT .= '$line_item_line';";
2404 push @template, '}',
2407 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2409 push @template, '[@--',
2410 ' foreach my $_tr_line (@total_items) {';
2412 while ( ( my $total_item_line = shift )
2413 !~ /^%%EndTotalDetails\s*$/ ) {
2414 $total_item_line =~ s/'/\\'/g; # nice LTS
2415 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2416 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2417 push @template, " \$OUT .= '$total_item_line';";
2420 push @template, '}',
2424 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2425 push @template, $line;
2431 warn "$_\n" foreach @template;
2440 #check for an invoice- specific override (eventually)
2442 #check for a customer- specific override
2443 return $self->cust_main->invoice_terms
2444 if $self->cust_main->invoice_terms;
2446 #use configured default or default default
2447 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2453 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2454 $duedate = $self->_date() + ( $1 * 86400 );
2461 $self->due_date ? time2str(shift, $self->due_date) : '';
2464 sub balance_due_msg {
2466 my $msg = 'Balance Due';
2467 return $msg unless $self->terms;
2468 if ( $self->due_date ) {
2469 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2470 } elsif ( $self->terms ) {
2471 $msg .= ' - '. $self->terms;
2476 =item invnum_date_pretty
2478 Returns a string with the invoice number and date, for example:
2479 "Invoice #54 (3/20/2008)"
2483 sub invnum_date_pretty {
2485 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2488 sub _items_sections {
2492 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2494 if ( $cust_bill_pkg->pkgnum > 0 ) {
2496 my $desc = $cust_bill_pkg->part_pkg->classname;
2498 $s{$desc} += $cust_bill_pkg->setup
2499 if ( $cust_bill_pkg->setup != 0 );
2501 $s{$desc} += $cust_bill_pkg->recur
2502 if ( $cust_bill_pkg->recur != 0 );
2508 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2515 #my @display = scalar(@_)
2517 # : qw( _items_previous _items_pkg );
2518 # #: qw( _items_pkg );
2519 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2520 my @display = qw( _items_previous _items_pkg );
2523 foreach my $display ( @display ) {
2524 push @b, $self->$display(@_);
2529 sub _items_previous {
2531 my $cust_main = $self->cust_main;
2532 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2534 foreach ( @pr_cust_bill ) {
2536 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2537 ' ('. time2str('%x',$_->_date). ')',
2538 #'pkgpart' => 'N/A',
2540 'amount' => sprintf("%.2f", $_->owed),
2546 # 'description' => 'Previous Balance',
2547 # #'pkgpart' => 'N/A',
2548 # 'pkgnum' => 'N/A',
2549 # 'amount' => sprintf("%10.2f", $pr_total ),
2550 # 'ext_description' => [ map {
2551 # "Invoice ". $_->invnum.
2552 # " (". time2str("%x",$_->_date). ") ".
2553 # sprintf("%10.2f", $_->owed)
2554 # } @pr_cust_bill ],
2562 my $section = delete $options{'section'};
2564 grep { $_->pkgnum &&
2566 ? $_->part_pkg->classname eq $section->{'description'}
2569 } $self->cust_bill_pkg;
2570 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2574 return 0 unless $a cmp $b;
2575 return -1 if $b eq 'Tax';
2576 return 1 if $a eq 'Tax';
2577 return -1 if $b eq 'Other surcharges';
2578 return 1 if $a eq 'Other surcharges';
2584 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2585 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2588 sub _items_cust_bill_pkg {
2590 my $cust_bill_pkg = shift;
2593 my $format = $opt{format} || '';
2594 my $escape_function = $opt{escape_function} || sub { shift };
2597 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2599 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2601 my $desc = $cust_bill_pkg->desc;
2603 my %details_opt = ( 'format' => $format,
2604 'escape_function' => $escape_function,
2607 if ( $cust_bill_pkg->pkgnum > 0 ) {
2609 if ( $cust_bill_pkg->setup != 0 ) {
2611 my $description = $desc;
2612 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2614 my @d = map &{$escape_function}($_),
2615 $cust_pkg->h_labels_short($self->_date);
2616 push @d, $cust_bill_pkg->details(%details_opt)
2617 if $cust_bill_pkg->recur == 0;
2620 description => $description,
2621 #pkgpart => $part_pkg->pkgpart,
2622 pkgnum => $cust_bill_pkg->pkgnum,
2623 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2624 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2625 quantity => $cust_bill_pkg->quantity,
2626 ext_description => \@d,
2630 if ( $cust_bill_pkg->recur != 0 ) {
2632 my $description = $desc;
2633 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2634 $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2635 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2638 #at least until cust_bill_pkg has "past" ranges in addition to
2639 #the "future" sdate/edate ones... see #3032
2640 my @d = map &{$escape_function}($_),
2641 $cust_pkg->h_labels_short($self->_date);
2642 #$cust_bill_pkg->edate,
2643 #$cust_bill_pkg->sdate),
2644 push @d, $cust_bill_pkg->details(%details_opt);
2647 description => $description,
2648 #pkgpart => $part_pkg->pkgpart,
2649 pkgnum => $cust_bill_pkg->pkgnum,
2650 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2651 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2652 quantity => $cust_bill_pkg->quantity,
2653 ext_description => \@d,
2658 } else { #pkgnum tax or one-shot line item (??)
2660 if ( $cust_bill_pkg->setup != 0 ) {
2662 'description' => $desc,
2663 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2666 if ( $cust_bill_pkg->recur != 0 ) {
2668 'description' => "$desc (".
2669 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2670 time2str("%x", $cust_bill_pkg->edate). ')',
2671 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2683 sub _items_credits {
2688 foreach ( $self->cust_credited ) {
2690 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2692 my $reason = $_->cust_credit->reason;
2693 #my $reason = substr($_->cust_credit->reason,0,32);
2694 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2695 $reason = " ($reason) " if $reason;
2697 #'description' => 'Credit ref\#'. $_->crednum.
2698 # " (". time2str("%x",$_->cust_credit->_date) .")".
2700 'description' => 'Credit applied '.
2701 time2str("%x",$_->cust_credit->_date). $reason,
2702 'amount' => sprintf("%.2f",$_->amount),
2705 #foreach ( @cr_cust_credit ) {
2707 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2708 # $money_char. sprintf("%10.2f",$_->credited)
2716 sub _items_payments {
2720 #get & print payments
2721 foreach ( $self->cust_bill_pay ) {
2723 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2726 'description' => "Payment received ".
2727 time2str("%x",$_->cust_pay->_date ),
2728 'amount' => sprintf("%.2f", $_->amount )
2747 sub process_reprint {
2748 process_re_X('print', @_);
2755 sub process_reemail {
2756 process_re_X('email', @_);
2764 process_re_X('fax', @_);
2767 use Storable qw(thaw);
2771 my( $method, $job ) = ( shift, shift );
2772 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2774 my $param = thaw(decode_base64(shift));
2775 warn Dumper($param) if $DEBUG;
2786 my($method, $job, %param ) = @_;
2788 warn "re_X $method for job $job with param:\n".
2789 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2792 #some false laziness w/search/cust_bill.html
2794 my $orderby = 'ORDER BY cust_bill._date';
2796 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2798 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2800 my @cust_bill = qsearch( {
2801 #'select' => "cust_bill.*",
2802 'table' => 'cust_bill',
2803 'addl_from' => $addl_from,
2805 'extra_sql' => $extra_sql,
2806 'order_by' => $orderby,
2810 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2813 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2814 foreach my $cust_bill ( @cust_bill ) {
2815 $cust_bill->$method();
2817 if ( $job ) { #progressbar foo
2819 if ( time - $min_sec > $last ) {
2820 my $error = $job->update_statustext(
2821 int( 100 * $num / scalar(@cust_bill) )
2823 die $error if $error;
2834 =head1 CLASS METHODS
2840 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2846 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2851 Returns an SQL fragment to retreive the net amount (charged minus credited).
2857 'charged - '. $class->credited_sql;
2862 Returns an SQL fragment to retreive the amount paid against this invoice.
2868 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2869 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2874 Returns an SQL fragment to retreive the amount credited against this invoice.
2880 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2881 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2884 =item search_sql HASHREF
2886 Class method which returns an SQL WHERE fragment to search for parameters
2887 specified in HASHREF. Valid parameters are
2893 Epoch date (UNIX timestamp) setting a lower bound for _date values
2897 Epoch date (UNIX timestamp) setting an upper bound for _date values
2911 =item newest_percust
2915 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2920 my($class, $param) = @_;
2922 warn "$me search_sql called with params: \n".
2923 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2928 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2929 push @search, "cust_bill._date >= $1";
2931 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2932 push @search, "cust_bill._date < $1";
2934 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2935 push @search, "cust_bill.invnum >= $1";
2937 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2938 push @search, "cust_bill.invnum <= $1";
2940 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2941 push @search, "cust_main.agentnum = $1";
2944 push @search, '0 != '. FS::cust_bill->owed_sql
2945 if $param->{'open'};
2947 push @search, '0 != '. FS::cust_bill->net_sql
2950 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2951 if $param->{'days'};
2953 if ( $param->{'newest_percust'} ) {
2955 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2956 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2958 my @newest_where = map { my $x = $_;
2959 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2962 grep ! /^cust_main./, @search;
2963 my $newest_where = scalar(@newest_where)
2964 ? ' AND '. join(' AND ', @newest_where)
2968 push @search, "cust_bill._date = (
2969 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2970 WHERE newest_cust_bill.custnum = cust_bill.custnum
2976 my $curuser = $FS::CurrentUser::CurrentUser;
2977 if ( $curuser->username eq 'fs_queue'
2978 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2980 my $newuser = qsearchs('access_user', {
2981 'username' => $username,
2985 $curuser = $newuser;
2987 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2991 push @search, $curuser->agentnums_sql;
2993 join(' AND ', @search );
3005 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3006 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base