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_invoice($template, $invoice_from)
842 $self->email($template, $invoice_from)
843 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
845 #$self->print_invoice($template)
846 $self->print($template)
847 if grep { $_ eq 'POST' } @invoicing_list; #postal
849 $self->fax_invoice($template)
850 if grep { $_ eq 'FAX' } @invoicing_list; #fax
856 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
860 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
862 INVOICE_FROM, if specified, overrides the default email invoice From: address.
866 sub queueable_email {
869 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
870 or die "invalid invoice number: " . $opt{invnum};
872 my @args = ( $opt{template} );
873 push @args, $opt{invoice_from}
874 if exists($opt{invoice_from}) && $opt{invoice_from};
876 my $error = $self->email( @args );
877 die $error if $error;
884 my $template = scalar(@_) ? shift : '';
888 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
890 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
891 $self->cust_main->invoicing_list;
893 #better to notify this person than silence
894 @invoicing_list = ($invoice_from) unless @invoicing_list;
896 my $error = send_email(
897 $self->generate_email(
898 'from' => $invoice_from,
899 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
900 'template' => $template,
903 die "can't email invoice: $error\n" if $error;
904 #die "$error\n" if $error;
908 =item lpr_data [ TEMPLATENAME ]
910 Returns the postscript or plaintext for this invoice as an arrayref.
912 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
917 my( $self, $template) = @_;
918 $conf->exists('invoice_latex')
919 ? [ $self->print_ps('', $template) ]
920 : [ $self->print_text('', $template) ];
923 =item print [ TEMPLATENAME ]
927 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
934 my $template = scalar(@_) ? shift : '';
936 do_print $self->lpr_data($template);
939 =item fax_invoice [ TEMPLATENAME ]
943 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
949 my $template = scalar(@_) ? shift : '';
951 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
952 unless $conf->exists('invoice_latex');
954 my $dialstring = $self->cust_main->getfield('fax');
957 my $error = send_fax( 'docdata' => $self->lpr_data($template),
958 'dialstring' => $dialstring,
960 die $error if $error;
964 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
966 Like B<send>, but only sends the invoice if it is the newest open invoice for
976 grep { $_->owed > 0 }
977 qsearch('cust_bill', {
978 'custnum' => $self->custnum,
979 #'_date' => { op=>'>', value=>$self->_date },
980 'invnum' => { op=>'>', value=>$self->invnum },
987 =item send_csv OPTION => VALUE, ...
989 Sends invoice as a CSV data-file to a remote host with the specified protocol.
993 protocol - currently only "ftp"
999 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1000 and YYMMDDHHMMSS is a timestamp.
1002 See L</print_csv> for a description of the output format.
1007 my($self, %opt) = @_;
1011 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1012 mkdir $spooldir, 0700 unless -d $spooldir;
1014 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1015 my $file = "$spooldir/$tracctnum.csv";
1017 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1019 open(CSV, ">$file") or die "can't open $file: $!";
1027 if ( $opt{protocol} eq 'ftp' ) {
1028 eval "use Net::FTP;";
1030 $net = Net::FTP->new($opt{server}) or die @$;
1032 die "unknown protocol: $opt{protocol}";
1035 $net->login( $opt{username}, $opt{password} )
1036 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1038 $net->binary or die "can't set binary mode";
1040 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1042 $net->put($file) or die "can't put $file: $!";
1052 Spools CSV invoice data.
1058 =item format - 'default' or 'billco'
1060 =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>).
1062 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1064 =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.
1071 my($self, %opt) = @_;
1073 my $cust_main = $self->cust_main;
1075 if ( $opt{'dest'} ) {
1076 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1077 $cust_main->invoicing_list;
1078 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1079 || ! keys %invoicing_list;
1082 if ( $opt{'balanceover'} ) {
1084 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1087 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1088 mkdir $spooldir, 0700 unless -d $spooldir;
1090 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1094 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1095 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1098 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1100 open(CSV, ">>$file") or die "can't open $file: $!";
1101 flock(CSV, LOCK_EX);
1106 if ( lc($opt{'format'}) eq 'billco' ) {
1108 flock(CSV, LOCK_UN);
1113 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1116 open(CSV,">>$file") or die "can't open $file: $!";
1117 flock(CSV, LOCK_EX);
1123 flock(CSV, LOCK_UN);
1130 =item print_csv OPTION => VALUE, ...
1132 Returns CSV data for this invoice.
1136 format - 'default' or 'billco'
1138 Returns a list consisting of two scalars. The first is a single line of CSV
1139 header information for this invoice. The second is one or more lines of CSV
1140 detail information for this invoice.
1142 If I<format> is not specified or "default", the fields of the CSV file are as
1145 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1149 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1151 B<record_type> is C<cust_bill> for the initial header line only. The
1152 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1153 fields are filled in.
1155 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1156 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1159 =item invnum - invoice number
1161 =item custnum - customer number
1163 =item _date - invoice date
1165 =item charged - total invoice amount
1167 =item first - customer first name
1169 =item last - customer first name
1171 =item company - company name
1173 =item address1 - address line 1
1175 =item address2 - address line 1
1185 =item pkg - line item description
1187 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1189 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1191 =item sdate - start date for recurring fee
1193 =item edate - end date for recurring fee
1197 If I<format> is "billco", the fields of the header CSV file are as follows:
1199 +-------------------------------------------------------------------+
1200 | FORMAT HEADER FILE |
1201 |-------------------------------------------------------------------|
1202 | Field | Description | Name | Type | Width |
1203 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1204 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1205 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1206 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1207 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1208 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1209 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1210 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1211 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1212 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1213 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1214 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1215 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1216 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1217 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1218 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1219 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1220 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1221 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1222 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1223 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1224 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1225 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1226 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1227 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1228 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1229 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1230 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1231 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1232 +-------+-------------------------------+------------+------+-------+
1234 If I<format> is "billco", the fields of the detail CSV file are as follows:
1236 FORMAT FOR DETAIL FILE
1238 Field | Description | Name | Type | Width
1239 1 | N/A-Leave Empty | RC | CHAR | 2
1240 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1241 3 | Account Number | TRACCTNUM | CHAR | 15
1242 4 | Invoice Number | TRINVOICE | CHAR | 15
1243 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1244 6 | Transaction Detail | DETAILS | CHAR | 100
1245 7 | Amount | AMT | NUM* | 9
1246 8 | Line Format Control** | LNCTRL | CHAR | 2
1247 9 | Grouping Code | GROUP | CHAR | 2
1248 10 | User Defined | ACCT CODE | CHAR | 15
1253 my($self, %opt) = @_;
1255 eval "use Text::CSV_XS";
1258 my $cust_main = $self->cust_main;
1260 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1262 if ( lc($opt{'format'}) eq 'billco' ) {
1265 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1267 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1269 my( $previous_balance, @unused ) = $self->previous; #previous balance
1271 my $pmt_cr_applied = 0;
1272 $pmt_cr_applied += $_->{'amount'}
1273 foreach ( $self->_items_payments, $self->_items_credits ) ;
1275 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1278 '', # 1 | N/A-Leave Empty CHAR 2
1279 '', # 2 | N/A-Leave Empty CHAR 15
1280 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1281 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1282 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1283 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1284 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1285 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1286 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1287 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1288 '', # 10 | Ancillary Billing Information CHAR 30
1289 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1290 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1293 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1296 $duedate, # 14 | Bill Due Date CHAR 10
1298 $previous_balance, # 15 | Previous Balance NUM* 9
1299 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1300 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1301 $totaldue, # 18 | Total Amt Due NUM* 9
1302 $totaldue, # 19 | Total Amt Due NUM* 9
1303 '', # 20 | 30 Day Aging NUM* 9
1304 '', # 21 | 60 Day Aging NUM* 9
1305 '', # 22 | 90 Day Aging NUM* 9
1306 'N', # 23 | Y/N CHAR 1
1307 '', # 24 | Remittance automation CHAR 100
1308 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1309 $self->custnum, # 26 | Customer Reference Number CHAR 15
1310 '0', # 27 | Federal Tax*** NUM* 9
1311 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1312 '0', # 29 | Other Taxes & Fees*** NUM* 9
1321 time2str("%x", $self->_date),
1322 sprintf("%.2f", $self->charged),
1323 ( map { $cust_main->getfield($_) }
1324 qw( first last company address1 address2 city state zip country ) ),
1326 ) or die "can't create csv";
1329 my $header = $csv->string. "\n";
1332 if ( lc($opt{'format'}) eq 'billco' ) {
1335 foreach my $item ( $self->_items_pkg ) {
1338 '', # 1 | N/A-Leave Empty CHAR 2
1339 '', # 2 | N/A-Leave Empty CHAR 15
1340 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1341 $self->invnum, # 4 | Invoice Number CHAR 15
1342 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1343 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1344 $item->{'amount'}, # 7 | Amount NUM* 9
1345 '', # 8 | Line Format Control** CHAR 2
1346 '', # 9 | Grouping Code CHAR 2
1347 '', # 10 | User Defined CHAR 15
1350 $detail .= $csv->string. "\n";
1356 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1358 my($pkg, $setup, $recur, $sdate, $edate);
1359 if ( $cust_bill_pkg->pkgnum ) {
1361 ($pkg, $setup, $recur, $sdate, $edate) = (
1362 $cust_bill_pkg->part_pkg->pkg,
1363 ( $cust_bill_pkg->setup != 0
1364 ? sprintf("%.2f", $cust_bill_pkg->setup )
1366 ( $cust_bill_pkg->recur != 0
1367 ? sprintf("%.2f", $cust_bill_pkg->recur )
1369 ( $cust_bill_pkg->sdate
1370 ? time2str("%x", $cust_bill_pkg->sdate)
1372 ($cust_bill_pkg->edate
1373 ?time2str("%x", $cust_bill_pkg->edate)
1377 } else { #pkgnum tax
1378 next unless $cust_bill_pkg->setup != 0;
1379 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1380 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1382 ($pkg, $setup, $recur, $sdate, $edate) =
1383 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1389 ( map { '' } (1..11) ),
1390 ($pkg, $setup, $recur, $sdate, $edate)
1391 ) or die "can't create csv";
1393 $detail .= $csv->string. "\n";
1399 ( $header, $detail );
1405 Pays this invoice with a compliemntary payment. If there is an error,
1406 returns the error, otherwise returns false.
1412 my $cust_pay = new FS::cust_pay ( {
1413 'invnum' => $self->invnum,
1414 'paid' => $self->owed,
1417 'payinfo' => $self->cust_main->payinfo,
1425 Attempts to pay this invoice with a credit card payment via a
1426 Business::OnlinePayment realtime gateway. See
1427 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1428 for supported processors.
1434 $self->realtime_bop( 'CC', @_ );
1439 Attempts to pay this invoice with an electronic check (ACH) payment via a
1440 Business::OnlinePayment realtime gateway. See
1441 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1442 for supported processors.
1448 $self->realtime_bop( 'ECHECK', @_ );
1453 Attempts to pay this invoice with phone bill (LEC) payment via a
1454 Business::OnlinePayment realtime gateway. See
1455 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1456 for supported processors.
1462 $self->realtime_bop( 'LEC', @_ );
1466 my( $self, $method ) = @_;
1468 my $cust_main = $self->cust_main;
1469 my $balance = $cust_main->balance;
1470 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1471 $amount = sprintf("%.2f", $amount);
1472 return "not run (balance $balance)" unless $amount > 0;
1474 my $description = 'Internet Services';
1475 if ( $conf->exists('business-onlinepayment-description') ) {
1476 my $dtempl = $conf->config('business-onlinepayment-description');
1478 my $agent_obj = $cust_main->agent
1479 or die "can't retreive agent for $cust_main (agentnum ".
1480 $cust_main->agentnum. ")";
1481 my $agent = $agent_obj->agent;
1482 my $pkgs = join(', ',
1483 map { $_->part_pkg->pkg }
1484 grep { $_->pkgnum } $self->cust_bill_pkg
1486 $description = eval qq("$dtempl");
1489 $cust_main->realtime_bop($method, $amount,
1490 'description' => $description,
1491 'invnum' => $self->invnum,
1496 =item batch_card OPTION => VALUE...
1498 Adds a payment for this invoice to the pending credit card batch (see
1499 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1500 runs the payment using a realtime gateway.
1505 my ($self, %options) = @_;
1506 my $cust_main = $self->cust_main;
1508 $options{invnum} = $self->invnum;
1510 $cust_main->batch_card(%options);
1513 sub _agent_template {
1515 $self->cust_main->agent_template;
1518 sub _agent_invoice_from {
1520 $self->cust_main->agent_invoice_from;
1523 =item print_text [ TIME [ , TEMPLATE ] ]
1525 Returns an text invoice, as a list of lines.
1527 TIME an optional value used to control the printing of overdue messages. The
1528 default is now. It isn't the date of the invoice; that's the `_date' field.
1529 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1530 L<Time::Local> and L<Date::Parse> for conversion functions.
1535 my( $self, $today, $template ) = @_;
1537 my %params = ( 'format' => 'template' );
1538 $params{'time'} = $today if $today;
1539 $params{'template'} = $template if $template;
1541 $self->print_generic( %params );
1544 =item print_latex [ TIME [ , TEMPLATE ] ]
1546 Internal method - returns a filename of a filled-in LaTeX template for this
1547 invoice (Note: add ".tex" to get the actual filename), and a filename of
1548 an associated logo (with the .eps extension included).
1550 See print_ps and print_pdf for methods that return PostScript and PDF output.
1552 TIME an optional value used to control the printing of overdue messages. The
1553 default is now. It isn't the date of the invoice; that's the `_date' field.
1554 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1555 L<Time::Local> and L<Date::Parse> for conversion functions.
1561 my( $self, $today, $template ) = @_;
1563 my %params = ( 'format' => 'latex' );
1564 $params{'time'} = $today if $today;
1565 $params{'template'} = $template if $template;
1567 $template ||= $self->_agent_template;
1569 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1570 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1574 ) or die "can't open temp file: $!\n";
1576 if ($template && $conf->exists("logo_${template}.eps")) {
1577 print $lh $conf->config_binary("logo_${template}.eps")
1578 or die "can't write temp file: $!\n";
1580 print $lh $conf->config_binary('logo.eps')
1581 or die "can't write temp file: $!\n";
1584 $params{'logo_file'} = $lh->filename;
1586 my @filled_in = $self->print_generic( %params );
1588 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1592 ) or die "can't open temp file: $!\n";
1593 print $fh join('', @filled_in );
1596 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1597 return ($1, $params{'logo_file'});
1601 =item print_generic OPTIONS_HASH
1603 Internal method - returns a filled-in template for this invoice as a scalar.
1605 See print_ps and print_pdf for methods that return PostScript and PDF output.
1607 Non optional options include
1608 format - latex, html, template
1610 Optional options include
1612 template - a value used as a suffix for a configuration template
1614 time - a value used to control the printing of overdue messages. The
1615 default is now. It isn't the date of the invoice; that's the `_date' field.
1616 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1617 L<Time::Local> and L<Date::Parse> for conversion functions.
1625 my( $self, %params ) = @_;
1626 my $today = $params{today} ? $params{today} : time;
1627 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1630 my $format = $params{format};
1631 die "Unknown format: $format"
1632 unless $format =~ /^(latex|html|template)$/;
1634 my $cust_main = $self->cust_main;
1635 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1636 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1639 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1640 'html' => [ '<%=', '%>' ],
1641 'template' => [ '{', '}' ],
1644 #create the template
1645 my $template = $params{template} ? $params{template} : $self->_agent_template;
1646 my $templatefile = "invoice_$format";
1647 $templatefile .= "_$template"
1648 if length($template);
1649 my @invoice_template = map "$_\n", $conf->config($templatefile)
1650 or die "cannot load config data $templatefile";
1653 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1654 #change this to a die when the old code is removed
1655 warn "old-style invoice template $templatefile; ".
1656 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1657 $old_latex = 'true';
1658 @invoice_template = _translate_old_latex_format(@invoice_template);
1661 my $text_template = new Text::Template(
1663 SOURCE => \@invoice_template,
1664 DELIMITERS => $delimiters{$format},
1667 $text_template->compile()
1668 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1671 # additional substitution could possibly cause breakage in existing templates
1672 my %convert_maps = (
1674 'notes' => sub { map "$_", @_ },
1675 'footer' => sub { map "$_", @_ },
1676 'smallfooter' => sub { map "$_", @_ },
1677 'returnaddress' => sub { map "$_", @_ },
1678 'coupon' => sub { map "$_", @_ },
1684 s/%%(.*)$/<!-- $1 -->/g;
1685 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1686 s/\\begin\{enumerate\}/<ol>/g;
1688 s/\\end\{enumerate\}/<\/ol>/g;
1689 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1698 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1700 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1705 s/\\\\\*?\s*$/<BR>/;
1706 s/\\hyphenation\{[\w\s\-]+}//;
1710 'coupon' => sub { "" },
1717 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1718 s/\\begin\{enumerate\}//g;
1720 s/\\end\{enumerate\}//g;
1721 s/\\textbf\{(.*)\}/$1/g;
1728 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1730 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1735 s/\\\\\*?\s*$/\n/; # dubious
1736 s/\\hyphenation\{[\w\s\-]+}//;
1740 'coupon' => sub { "" },
1745 # hashes for differing output formats
1746 my %nbsps = ( 'latex' => '~',
1747 'html' => '', # '&nbps;' would be nice
1748 'template' => '', # not used
1750 my $nbsp = $nbsps{$format};
1752 my %escape_functions = ( 'latex' => \&_latex_escape,
1753 'html' => \&encode_entities,
1754 'template' => sub { shift },
1756 my $escape_function = $escape_functions{$format};
1758 my %date_formats = ( 'latex' => '%b %o, %Y',
1759 'html' => '%b %o, %Y',
1762 my $date_format = $date_formats{$format};
1764 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1766 'html' => sub { return '<b>'. shift(). '</b>'
1768 'template' => sub { shift },
1770 my $embolden_function = $embolden_functions{$format};
1773 # generate template variables
1776 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1780 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1786 $returnaddress = join("\n",
1787 $conf->config_orbase("invoice_${format}returnaddress", $template)
1790 } elsif ( grep /\S/,
1791 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1793 my $convert_map = $convert_maps{$format}{'returnaddress'};
1796 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1801 } elsif ( grep /\S/, $conf->config('company_address') ) {
1803 my $convert_map = $convert_maps{$format}{'returnaddress'};
1804 $returnaddress = join( "\n", &$convert_map(
1805 map { s/( {2,})/'~' x length($1)/eg;
1809 ( $conf->config('company_name'),
1810 $conf->config('company_address'),
1817 my $warning = "Couldn't find a return address; ".
1818 "do you need to set the company_address configuration value?";
1820 $returnaddress = $nbsp;
1821 #$returnaddress = $warning;
1825 my %invoice_data = (
1826 'company_name' => scalar( $conf->config('company_name') ),
1827 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1828 'custnum' => $self->custnum,
1829 'invnum' => $self->invnum,
1830 'date' => time2str($date_format, $self->_date),
1831 'today' => time2str('%b %o, %Y', $today),
1832 'agent' => &$escape_function($cust_main->agent->agent),
1833 'agent_custid' => &$escape_function($cust_main->agent_custid),
1834 'payname' => &$escape_function($cust_main->payname),
1835 'company' => &$escape_function($cust_main->company),
1836 'address1' => &$escape_function($cust_main->address1),
1837 'address2' => &$escape_function($cust_main->address2),
1838 'city' => &$escape_function($cust_main->city),
1839 'state' => &$escape_function($cust_main->state),
1840 'zip' => &$escape_function($cust_main->zip),
1841 'returnaddress' => $returnaddress,
1843 'terms' => $self->terms,
1844 'template' => $params{'template'},
1845 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1846 # better hang on to conf_dir for a while
1847 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1850 'current_charges' => sprintf("%.2f", $self->charged),
1851 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1852 'ship_enable' => $conf->exists('invoice-ship_address'),
1853 'unitprices' => $conf->exists('invoice-unitprice'),
1856 my $countrydefault = $conf->config('countrydefault') || 'US';
1857 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1858 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1859 my $method = $prefix.$_;
1860 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1862 $invoice_data{'ship_country'} = ''
1863 if ( $invoice_data{'ship_country'} eq $countrydefault );
1865 $invoice_data{'cid'} = $params{'cid'}
1868 if ( $cust_main->country eq $countrydefault ) {
1869 $invoice_data{'country'} = '';
1871 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1875 $invoice_data{'address'} = \@address;
1877 $cust_main->payname.
1878 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1879 ? " (P.O. #". $cust_main->payinfo. ")"
1883 push @address, $cust_main->company
1884 if $cust_main->company;
1885 push @address, $cust_main->address1;
1886 push @address, $cust_main->address2
1887 if $cust_main->address2;
1889 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1890 push @address, $invoice_data{'country'}
1891 if $invoice_data{'country'};
1893 while (scalar(@address) < 5);
1895 $invoice_data{'logo_file'} = $params{'logo_file'}
1896 if $params{'logo_file'};
1898 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1899 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1900 #my $balance_due = $self->owed + $pr_total - $cr_total;
1901 my $balance_due = $self->owed + $pr_total;
1902 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1903 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1905 #do variable substitution in notes, footer, smallfooter
1906 foreach my $include (qw( notes footer smallfooter coupon )) {
1908 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1911 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1913 @inc_src = $conf->config($inc_file);
1917 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1919 my $convert_map = $convert_maps{$format}{$include};
1921 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1922 s/--\@\]/$delimiters{$format}[1]/g;
1925 &$convert_map( $conf->config($inc_file) );
1929 my $inc_tt = new Text::Template (
1931 SOURCE => [ map "$_\n", @inc_src ],
1932 DELIMITERS => $delimiters{$format},
1933 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1935 unless ( $inc_tt->compile() ) {
1936 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1937 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1941 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1943 $invoice_data{$include} =~ s/\n+$//
1944 if ($format eq 'latex');
1947 $invoice_data{'po_line'} =
1948 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1949 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1952 my %money_chars = ( 'latex' => '',
1953 'html' => $conf->config('money_char') || '$',
1956 my $money_char = $money_chars{$format};
1958 my %other_money_chars = ( 'latex' => '\dollar ',
1959 'html' => $conf->config('money_char') || '$',
1962 my $other_money_char = $other_money_chars{$format};
1964 my @detail_items = ();
1965 my @total_items = ();
1969 $invoice_data{'detail_items'} = \@detail_items;
1970 $invoice_data{'total_items'} = \@total_items;
1971 $invoice_data{'buf'} = \@buf;
1972 $invoice_data{'sections'} = \@sections;
1974 my $previous_section = { 'description' => 'Previous Charges',
1975 'subtotal' => $other_money_char.
1976 sprintf('%.2f', $pr_total),
1980 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1981 'subtotal' => $taxtotal }; # adjusted below
1983 my $adjusttotal = 0;
1984 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
1985 'subtotal' => 0 }; # adjusted below
1987 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
1988 if ( $multisection ) {
1989 push @sections, $self->_items_sections;
1991 push @sections, { 'description' => '', 'subtotal' => '' };
1994 foreach my $line_item ( $conf->exists('disable_previous_balance')
1996 : $self->_items_previous
2000 ext_description => [],
2002 $detail->{'ref'} = $line_item->{'pkgnum'};
2003 $detail->{'quantity'} = 1;
2004 $detail->{'section'} = $previous_section;
2005 $detail->{'description'} = &$escape_function($line_item->{'description'});
2006 if ( exists $line_item->{'ext_description'} ) {
2007 @{$detail->{'ext_description'}} = map {
2008 &$escape_function($_);
2009 } @{$line_item->{'ext_description'}};
2011 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2012 $line_item->{'amount'};
2013 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2015 push @detail_items, $detail;
2016 push @buf, [ $detail->{'description'},
2017 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2021 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2022 push @buf, ['','-----------'];
2023 push @buf, [ 'Total Previous Balance',
2024 $money_char. sprintf("%10.2f", $pr_total) ];
2028 foreach my $section (@sections) {
2030 $section->{'subtotal'} = $other_money_char.
2031 sprintf('%.2f', $section->{'subtotal'})
2034 if ( $section->{'description'} ) {
2035 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2041 $options{'section'} = $section if $multisection;
2042 $options{'format'} = $format;
2043 $options{'escape_function'} = $escape_function;
2045 foreach my $line_item ( $self->_items_pkg(%options) ) {
2047 ext_description => [],
2049 $detail->{'ref'} = $line_item->{'pkgnum'};
2050 $detail->{'quantity'} = $line_item->{'quantity'};
2051 $detail->{'section'} = $section;
2052 $detail->{'description'} = &$escape_function($line_item->{'description'});
2053 if ( exists $line_item->{'ext_description'} ) {
2054 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2056 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2057 $line_item->{'amount'};
2058 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2059 $line_item->{'unit_amount'};
2060 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2062 push @detail_items, $detail;
2063 push @buf, ( [ $detail->{'description'},
2064 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2066 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2070 if ( $section->{'description'} ) {
2071 push @buf, ( ['','-----------'],
2072 [ $section->{'description'}. ' sub-total',
2073 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2082 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2083 unshift @sections, $previous_section if $pr_total;
2086 foreach my $tax ( $self->_items_tax ) {
2088 $total->{'total_item'} = &$escape_function($tax->{'description'});
2089 $taxtotal += $tax->{'amount'};
2090 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2091 if ( $multisection ) {
2092 my $money = $old_latex ? '' : $money_char;
2093 push @detail_items, {
2094 ext_description => [],
2097 description => &$escape_function($tax->{'description'}),
2098 amount => $money. $tax->{'amount'},
2100 section => $tax_section,
2103 push @total_items, $total;
2105 push @buf,[ $total->{'total_item'},
2106 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2113 $total->{'total_item'} = 'Sub-total';
2114 $total->{'total_amount'} =
2115 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2117 if ( $multisection ) {
2118 $tax_section->{'subtotal'} = $other_money_char.
2119 sprintf('%.2f', $taxtotal);
2120 $tax_section->{'pretotal'} = 'New charges sub-total '.
2121 $total->{'total_amount'};
2122 push @sections, $tax_section if $taxtotal;
2124 unshift @total_items, $total;
2127 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2129 push @buf,['','-----------'];
2130 push @buf,[( $conf->exists('disable_previous_balance')
2132 : 'Total New Charges'
2134 $money_char. sprintf("%10.2f",$self->charged) ];
2139 $total->{'total_item'} = &$embolden_function('Total');
2140 $total->{'total_amount'} =
2141 &$embolden_function(
2144 $self->charged + ( $conf->exists('disable_previous_balance')
2150 if ( $multisection ) {
2151 $adjust_section->{'pretotal'} = 'New charges total '.
2152 $total->{'total_amount'};
2154 push @total_items, $total;
2156 push @buf,['','-----------'];
2157 push @buf,['Total Charges',
2159 sprintf( '%10.2f', $self->charged +
2160 ( $conf->exists('disable_previous_balance')
2169 unless ( $conf->exists('disable_previous_balance') ) {
2170 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2173 my $credittotal = 0;
2174 foreach my $credit ( $self->_items_credits ) {
2176 $total->{'total_item'} = &$escape_function($credit->{'description'});
2177 $credittotal += $credit->{'amount'};
2178 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2179 $adjusttotal += $credit->{'amount'};
2180 if ( $multisection ) {
2181 my $money = $old_latex ? '' : $money_char;
2182 push @detail_items, {
2183 ext_description => [],
2186 description => &$escape_function($credit->{'description'}),
2187 amount => $money. $credit->{'amount'},
2189 section => $adjust_section,
2192 push @total_items, $total;
2195 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2198 foreach ( $self->cust_credited ) {
2200 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2202 my $reason = substr($_->cust_credit->reason,0,32);
2203 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2204 $reason = " ($reason) " if $reason;
2206 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2207 $money_char. sprintf("%10.2f",$_->amount)
2212 my $paymenttotal = 0;
2213 foreach my $payment ( $self->_items_payments ) {
2215 $total->{'total_item'} = &$escape_function($payment->{'description'});
2216 $paymenttotal += $payment->{'amount'};
2217 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2218 $adjusttotal += $payment->{'amount'};
2219 if ( $multisection ) {
2220 my $money = $old_latex ? '' : $money_char;
2221 push @detail_items, {
2222 ext_description => [],
2225 description => &$escape_function($payment->{'description'}),
2226 amount => $money. $payment->{'amount'},
2228 section => $adjust_section,
2231 push @total_items, $total;
2233 push @buf, [ $payment->{'description'},
2234 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2237 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2239 if ( $multisection ) {
2240 $adjust_section->{'subtotal'} = $other_money_char.
2241 sprintf('%.2f', $adjusttotal);
2242 push @sections, $adjust_section;
2247 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2248 $total->{'total_amount'} =
2249 &$embolden_function(
2250 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2252 if ( $multisection ) {
2253 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2254 $total->{'total_amount'};
2256 push @total_items, $total;
2258 push @buf,['','-----------'];
2259 push @buf,[$self->balance_due_msg, $money_char.
2260 sprintf("%10.2f", $balance_due ) ];
2266 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2267 /invoice_lines\((\d*)\)/;
2268 $invoice_lines += $1 || scalar(@buf);
2271 die "no invoice_lines() functions in template?"
2272 if ( $format eq 'template' && !$wasfunc );
2274 if ($format eq 'template') {
2276 if ( $invoice_lines ) {
2277 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2278 $invoice_data{'total_pages'}++
2279 if scalar(@buf) % $invoice_lines;
2282 #setup subroutine for the template
2283 sub FS::cust_bill::_template::invoice_lines {
2284 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2286 scalar(@FS::cust_bill::_template::buf)
2287 ? shift @FS::cust_bill::_template::buf
2296 push @collect, split("\n",
2297 $text_template->fill_in( HASH => \%invoice_data,
2298 PACKAGE => 'FS::cust_bill::_template'
2301 $FS::cust_bill::_template::page++;
2303 map "$_\n", @collect;
2305 warn "filling in template for invoice ". $self->invnum. "\n"
2307 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2310 $text_template->fill_in(HASH => \%invoice_data);
2314 =item print_ps [ TIME [ , TEMPLATE ] ]
2316 Returns an postscript invoice, as a scalar.
2318 TIME an optional value used to control the printing of overdue messages. The
2319 default is now. It isn't the date of the invoice; that's the `_date' field.
2320 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2321 L<Time::Local> and L<Date::Parse> for conversion functions.
2328 my ($file, $lfile) = $self->print_latex(@_);
2329 my $ps = generate_ps($file);
2335 =item print_pdf [ TIME [ , TEMPLATE ] ]
2337 Returns an PDF invoice, as a scalar.
2339 TIME an optional value used to control the printing of overdue messages. The
2340 default is now. It isn't the date of the invoice; that's the `_date' field.
2341 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2342 L<Time::Local> and L<Date::Parse> for conversion functions.
2349 my ($file, $lfile) = $self->print_latex(@_);
2350 my $pdf = generate_pdf($file);
2356 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2358 Returns an HTML invoice, as a scalar.
2360 TIME an optional value used to control the printing of overdue messages. The
2361 default is now. It isn't the date of the invoice; that's the `_date' field.
2362 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2363 L<Time::Local> and L<Date::Parse> for conversion functions.
2365 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2366 when emailing the invoice as part of a multipart/related MIME email.
2371 my( $self, $today, $template, $cid ) = @_;
2373 my %params = ( 'format' => 'html' );
2374 $params{'time'} = $today if $today;
2375 $params{'template'} = $template if $template;
2376 $params{'cid'} = $cid if $cid;
2378 $self->print_generic( %params );
2381 # quick subroutine for print_latex
2383 # There are ten characters that LaTeX treats as special characters, which
2384 # means that they do not simply typeset themselves:
2385 # # $ % & ~ _ ^ \ { }
2387 # TeX ignores blanks following an escaped character; if you want a blank (as
2388 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2392 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2393 $value =~ s/([<>])/\$$1\$/g;
2397 #utility methods for print_*
2399 sub _translate_old_latex_format {
2400 warn "_translate_old_latex_format called\n"
2407 if ( $line =~ /^%%Detail\s*$/ ) {
2409 push @template, q![@--!,
2410 q! foreach my $_tr_line (@detail_items) {!,
2411 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2412 q! $_tr_line->{'description'} .= !,
2413 q! "\\tabularnewline\n~~".!,
2414 q! join( "\\tabularnewline\n~~",!,
2415 q! @{$_tr_line->{'ext_description'}}!,
2419 while ( ( my $line_item_line = shift )
2420 !~ /^%%EndDetail\s*$/ ) {
2421 $line_item_line =~ s/'/\\'/g; # nice LTS
2422 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2423 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2424 push @template, " \$OUT .= '$line_item_line';";
2427 push @template, '}',
2430 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2432 push @template, '[@--',
2433 ' foreach my $_tr_line (@total_items) {';
2435 while ( ( my $total_item_line = shift )
2436 !~ /^%%EndTotalDetails\s*$/ ) {
2437 $total_item_line =~ s/'/\\'/g; # nice LTS
2438 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2439 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2440 push @template, " \$OUT .= '$total_item_line';";
2443 push @template, '}',
2447 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2448 push @template, $line;
2454 warn "$_\n" foreach @template;
2463 #check for an invoice- specific override (eventually)
2465 #check for a customer- specific override
2466 return $self->cust_main->invoice_terms
2467 if $self->cust_main->invoice_terms;
2469 #use configured default or default default
2470 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2476 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2477 $duedate = $self->_date() + ( $1 * 86400 );
2484 $self->due_date ? time2str(shift, $self->due_date) : '';
2487 sub balance_due_msg {
2489 my $msg = 'Balance Due';
2490 return $msg unless $self->terms;
2491 if ( $self->due_date ) {
2492 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2493 } elsif ( $self->terms ) {
2494 $msg .= ' - '. $self->terms;
2499 sub balance_due_date {
2502 if ( $conf->exists('invoice_default_terms')
2503 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2504 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2509 =item invnum_date_pretty
2511 Returns a string with the invoice number and date, for example:
2512 "Invoice #54 (3/20/2008)"
2516 sub invnum_date_pretty {
2518 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2521 sub _items_sections {
2525 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2527 if ( $cust_bill_pkg->pkgnum > 0 ) {
2529 my $desc = $cust_bill_pkg->part_pkg->categoryname;
2531 $s{$desc} += $cust_bill_pkg->setup
2532 if ( $cust_bill_pkg->setup != 0 );
2534 $s{$desc} += $cust_bill_pkg->recur
2535 if ( $cust_bill_pkg->recur != 0 );
2541 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2548 #my @display = scalar(@_)
2550 # : qw( _items_previous _items_pkg );
2551 # #: qw( _items_pkg );
2552 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2553 my @display = qw( _items_previous _items_pkg );
2556 foreach my $display ( @display ) {
2557 push @b, $self->$display(@_);
2562 sub _items_previous {
2564 my $cust_main = $self->cust_main;
2565 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2567 foreach ( @pr_cust_bill ) {
2569 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2570 ' ('. time2str('%x',$_->_date). ')',
2571 #'pkgpart' => 'N/A',
2573 'amount' => sprintf("%.2f", $_->owed),
2579 # 'description' => 'Previous Balance',
2580 # #'pkgpart' => 'N/A',
2581 # 'pkgnum' => 'N/A',
2582 # 'amount' => sprintf("%10.2f", $pr_total ),
2583 # 'ext_description' => [ map {
2584 # "Invoice ". $_->invnum.
2585 # " (". time2str("%x",$_->_date). ") ".
2586 # sprintf("%10.2f", $_->owed)
2587 # } @pr_cust_bill ],
2595 my $section = delete $options{'section'};
2597 grep { $_->pkgnum &&
2599 ? $_->part_pkg->categoryname eq $section->{'description'}
2602 } $self->cust_bill_pkg;
2603 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2607 return 0 unless $a cmp $b;
2608 return -1 if $b eq 'Tax';
2609 return 1 if $a eq 'Tax';
2610 return -1 if $b eq 'Other surcharges';
2611 return 1 if $a eq 'Other surcharges';
2617 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2618 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2621 sub _items_cust_bill_pkg {
2623 my $cust_bill_pkg = shift;
2626 my $format = $opt{format} || '';
2627 my $escape_function = $opt{escape_function} || sub { shift };
2630 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2632 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2634 my $desc = $cust_bill_pkg->desc;
2636 my %details_opt = ( 'format' => $format,
2637 'escape_function' => $escape_function,
2640 if ( $cust_bill_pkg->pkgnum > 0 ) {
2642 if ( $cust_bill_pkg->setup != 0 ) {
2644 my $description = $desc;
2645 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2647 my @d = map &{$escape_function}($_),
2648 $cust_pkg->h_labels_short($self->_date);
2649 push @d, $cust_bill_pkg->details(%details_opt)
2650 if $cust_bill_pkg->recur == 0;
2653 description => $description,
2654 #pkgpart => $part_pkg->pkgpart,
2655 pkgnum => $cust_bill_pkg->pkgnum,
2656 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2657 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2658 quantity => $cust_bill_pkg->quantity,
2659 ext_description => \@d,
2663 if ( $cust_bill_pkg->recur != 0 ) {
2665 my $description = $desc;
2666 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2667 $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2668 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2671 #at least until cust_bill_pkg has "past" ranges in addition to
2672 #the "future" sdate/edate ones... see #3032
2673 my @d = map &{$escape_function}($_),
2674 $cust_pkg->h_labels_short($self->_date);
2675 #$cust_bill_pkg->edate,
2676 #$cust_bill_pkg->sdate),
2677 push @d, $cust_bill_pkg->details(%details_opt);
2680 description => $description,
2681 #pkgpart => $part_pkg->pkgpart,
2682 pkgnum => $cust_bill_pkg->pkgnum,
2683 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2684 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2685 quantity => $cust_bill_pkg->quantity,
2686 ext_description => \@d,
2691 } else { #pkgnum tax or one-shot line item (??)
2693 if ( $cust_bill_pkg->setup != 0 ) {
2695 'description' => $desc,
2696 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2699 if ( $cust_bill_pkg->recur != 0 ) {
2701 'description' => "$desc (".
2702 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2703 time2str("%x", $cust_bill_pkg->edate). ')',
2704 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2716 sub _items_credits {
2721 foreach ( $self->cust_credited ) {
2723 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2725 my $reason = $_->cust_credit->reason;
2726 #my $reason = substr($_->cust_credit->reason,0,32);
2727 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2728 $reason = " ($reason) " if $reason;
2730 #'description' => 'Credit ref\#'. $_->crednum.
2731 # " (". time2str("%x",$_->cust_credit->_date) .")".
2733 'description' => 'Credit applied '.
2734 time2str("%x",$_->cust_credit->_date). $reason,
2735 'amount' => sprintf("%.2f",$_->amount),
2738 #foreach ( @cr_cust_credit ) {
2740 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2741 # $money_char. sprintf("%10.2f",$_->credited)
2749 sub _items_payments {
2753 #get & print payments
2754 foreach ( $self->cust_bill_pay ) {
2756 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2759 'description' => "Payment received ".
2760 time2str("%x",$_->cust_pay->_date ),
2761 'amount' => sprintf("%.2f", $_->amount )
2780 sub process_reprint {
2781 process_re_X('print', @_);
2788 sub process_reemail {
2789 process_re_X('email', @_);
2797 process_re_X('fax', @_);
2800 use Storable qw(thaw);
2804 my( $method, $job ) = ( shift, shift );
2805 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2807 my $param = thaw(decode_base64(shift));
2808 warn Dumper($param) if $DEBUG;
2819 my($method, $job, %param ) = @_;
2821 warn "re_X $method for job $job with param:\n".
2822 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2825 #some false laziness w/search/cust_bill.html
2827 my $orderby = 'ORDER BY cust_bill._date';
2829 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2831 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2833 my @cust_bill = qsearch( {
2834 #'select' => "cust_bill.*",
2835 'table' => 'cust_bill',
2836 'addl_from' => $addl_from,
2838 'extra_sql' => $extra_sql,
2839 'order_by' => $orderby,
2843 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2846 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2847 foreach my $cust_bill ( @cust_bill ) {
2848 $cust_bill->$method();
2850 if ( $job ) { #progressbar foo
2852 if ( time - $min_sec > $last ) {
2853 my $error = $job->update_statustext(
2854 int( 100 * $num / scalar(@cust_bill) )
2856 die $error if $error;
2867 =head1 CLASS METHODS
2873 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2879 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2884 Returns an SQL fragment to retreive the net amount (charged minus credited).
2890 'charged - '. $class->credited_sql;
2895 Returns an SQL fragment to retreive the amount paid against this invoice.
2901 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2902 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2907 Returns an SQL fragment to retreive the amount credited against this invoice.
2913 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2914 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2917 =item search_sql HASHREF
2919 Class method which returns an SQL WHERE fragment to search for parameters
2920 specified in HASHREF. Valid parameters are
2926 Epoch date (UNIX timestamp) setting a lower bound for _date values
2930 Epoch date (UNIX timestamp) setting an upper bound for _date values
2944 =item newest_percust
2948 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2953 my($class, $param) = @_;
2955 warn "$me search_sql called with params: \n".
2956 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2961 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2962 push @search, "cust_bill._date >= $1";
2964 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2965 push @search, "cust_bill._date < $1";
2967 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2968 push @search, "cust_bill.invnum >= $1";
2970 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2971 push @search, "cust_bill.invnum <= $1";
2973 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2974 push @search, "cust_main.agentnum = $1";
2977 push @search, '0 != '. FS::cust_bill->owed_sql
2978 if $param->{'open'};
2980 push @search, '0 != '. FS::cust_bill->net_sql
2983 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2984 if $param->{'days'};
2986 if ( $param->{'newest_percust'} ) {
2988 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2989 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2991 my @newest_where = map { my $x = $_;
2992 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2995 grep ! /^cust_main./, @search;
2996 my $newest_where = scalar(@newest_where)
2997 ? ' AND '. join(' AND ', @newest_where)
3001 push @search, "cust_bill._date = (
3002 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3003 WHERE newest_cust_bill.custnum = cust_bill.custnum
3009 my $curuser = $FS::CurrentUser::CurrentUser;
3010 if ( $curuser->username eq 'fs_queue'
3011 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3013 my $newuser = qsearchs('access_user', {
3014 'username' => $username,
3018 $curuser = $newuser;
3020 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3024 push @search, $curuser->agentnums_sql;
3026 join(' AND ', @search );
3038 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3039 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base