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;
28 use FS::cust_bill_pay;
29 use FS::cust_bill_pay_batch;
30 use FS::part_bill_event;
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
36 $me = '[FS::cust_bill]';
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub {
41 $money_char = $conf->config('money_char') || '$';
46 FS::cust_bill - Object methods for cust_bill records
52 $record = new FS::cust_bill \%hash;
53 $record = new FS::cust_bill { 'column' => 'value' };
55 $error = $record->insert;
57 $error = $new_record->replace($old_record);
59 $error = $record->delete;
61 $error = $record->check;
63 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
65 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
67 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
69 @cust_pay_objects = $cust_bill->cust_pay;
71 $tax_amount = $record->tax;
73 @lines = $cust_bill->print_text;
74 @lines = $cust_bill->print_text $time;
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
81 following fields are currently supported:
85 =item invnum - primary key (assigned automatically for new invoices)
87 =item custnum - customer (see L<FS::cust_main>)
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
92 =item charged - amount of this invoice
94 =item printed - deprecated
96 =item closed - books closed flag, empty or `Y'
106 Creates a new invoice. To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
112 sub table { 'cust_bill'; }
114 sub cust_linked { $_[0]->cust_main_custnum; }
115 sub cust_unlinked_msg {
117 "WARNING: can't find cust_main.custnum ". $self->custnum.
118 ' (cust_bill.invnum '. $self->invnum. ')';
123 Adds this invoice to the database ("Posts" the invoice). If there is an error,
124 returns the error, otherwise returns false.
128 This method now works but you probably shouldn't use it. Instead, apply a
129 credit against the invoice.
131 Using this method to delete invoices outright is really, really bad. There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
135 Really, don't use it.
141 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
142 $self->SUPER::delete(@_);
145 =item replace OLD_RECORD
147 Replaces the OLD_RECORD with this one in the database. If there is an error,
148 returns the error, otherwise returns false.
150 Only printed may be changed. printed is normally updated by calling the
151 collect method of a customer object (see L<FS::cust_main>).
155 #replace can be inherited from Record.pm
157 # replace_check is now the preferred way to #implement replace data checks
158 # (so $object->replace() works without an argument)
161 my( $new, $old ) = ( shift, shift );
162 return "Can't change custnum!" unless $old->custnum == $new->custnum;
163 #return "Can't change _date!" unless $old->_date eq $new->_date;
164 return "Can't change _date!" unless $old->_date == $new->_date;
165 return "Can't change charged!" unless $old->charged == $new->charged
166 || $old->charged == 0;
173 Checks all fields to make sure this is a valid invoice. If there is an error,
174 returns the error, otherwise returns false. Called by the insert and replace
183 $self->ut_numbern('invnum')
184 || $self->ut_number('custnum')
185 || $self->ut_numbern('_date')
186 || $self->ut_money('charged')
187 || $self->ut_numbern('printed')
188 || $self->ut_enum('closed', [ '', 'Y' ])
190 return $error if $error;
192 return "Unknown customer"
193 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
195 $self->_date(time) unless $self->_date;
197 $self->printed(0) if $self->printed eq '';
204 Returns a list consisting of the total previous balance for this customer,
205 followed by the previous outstanding invoices (as FS::cust_bill objects also).
212 my @cust_bill = sort { $a->_date <=> $b->_date }
213 grep { $_->owed != 0 && $_->_date < $self->_date }
214 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
216 foreach ( @cust_bill ) { $total += $_->owed; }
222 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
228 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
233 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
240 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
242 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
245 =item open_cust_bill_pkg
247 Returns the open line items for this invoice.
249 Note that cust_bill_pkg with both setup and recur fees are returned as two
250 separate line items, each with only one fee.
254 # modeled after cust_main::open_cust_bill
255 sub open_cust_bill_pkg {
258 # grep { $_->owed > 0 } $self->cust_bill_pkg
260 my %other = ( 'recur' => 'setup',
261 'setup' => 'recur', );
263 foreach my $field ( qw( recur setup )) {
264 push @open, map { $_->set( $other{$field}, 0 ); $_; }
265 grep { $_->owed($field) > 0 }
266 $self->cust_bill_pkg;
272 =item cust_bill_event
274 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
279 sub cust_bill_event {
281 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
287 Returns the customer (see L<FS::cust_main>) for this invoice.
293 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
296 =item cust_suspend_if_balance_over AMOUNT
298 Suspends the customer associated with this invoice if the total amount owed on
299 this invoice and all older invoices is greater than the specified amount.
301 Returns a list: an empty list on success or a list of errors.
305 sub cust_suspend_if_balance_over {
306 my( $self, $amount ) = ( shift, shift );
307 my $cust_main = $self->cust_main;
308 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
311 $cust_main->suspend(@_);
317 Depreciated. See the cust_credited method.
319 #Returns a list consisting of the total previous credited (see
320 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
321 #outstanding credits (FS::cust_credit objects).
327 croak "FS::cust_bill->cust_credit depreciated; see ".
328 "FS::cust_bill->cust_credit_bill";
331 #my @cust_credit = sort { $a->_date <=> $b->_date }
332 # grep { $_->credited != 0 && $_->_date < $self->_date }
333 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
335 #foreach (@cust_credit) { $total += $_->credited; }
336 #$total, @cust_credit;
341 Depreciated. See the cust_bill_pay method.
343 #Returns all payments (see L<FS::cust_pay>) for this invoice.
349 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
351 #sort { $a->_date <=> $b->_date }
352 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
358 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
364 sort { $a->_date <=> $b->_date }
365 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
370 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
376 sort { $a->_date <=> $b->_date }
377 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
383 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
390 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
392 foreach (@taxlines) { $total += $_->setup; }
398 Returns the amount owed (still outstanding) on this invoice, which is charged
399 minus all payment applications (see L<FS::cust_bill_pay>) and credit
400 applications (see L<FS::cust_credit_bill>).
406 my $balance = $self->charged;
407 $balance -= $_->amount foreach ( $self->cust_bill_pay );
408 $balance -= $_->amount foreach ( $self->cust_credited );
409 $balance = sprintf( "%.2f", $balance);
410 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
414 =item apply_payments_and_credits
418 sub apply_payments_and_credits {
421 local $SIG{HUP} = 'IGNORE';
422 local $SIG{INT} = 'IGNORE';
423 local $SIG{QUIT} = 'IGNORE';
424 local $SIG{TERM} = 'IGNORE';
425 local $SIG{TSTP} = 'IGNORE';
426 local $SIG{PIPE} = 'IGNORE';
428 my $oldAutoCommit = $FS::UID::AutoCommit;
429 local $FS::UID::AutoCommit = 0;
432 $self->select_for_update; #mutex
434 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
435 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
437 while ( $self->owed > 0 and ( @payments || @credits ) ) {
440 if ( @payments && @credits ) {
442 #decide which goes first by weight of top (unapplied) line item
444 my @open_lineitems = $self->open_cust_bill_pkg;
447 max( map { $_->part_pkg->pay_weight || 0 }
452 my $max_credit_weight =
453 max( map { $_->part_pkg->credit_weight || 0 }
459 #if both are the same... payments first? it has to be something
460 if ( $max_pay_weight >= $max_credit_weight ) {
466 } elsif ( @payments ) {
468 } elsif ( @credits ) {
471 die "guru meditation #12 and 35";
474 if ( $app eq 'pay' ) {
476 my $payment = shift @payments;
478 $app = new FS::cust_bill_pay {
479 'paynum' => $payment->paynum,
480 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
483 } elsif ( $app eq 'credit' ) {
485 my $credit = shift @credits;
487 $app = new FS::cust_credit_bill {
488 'crednum' => $credit->crednum,
489 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
493 die "guru meditation #12 and 35";
496 $app->invnum( $self->invnum );
498 my $error = $app->insert;
500 $dbh->rollback if $oldAutoCommit;
501 return "Error inserting ". $app->table. " record: $error";
503 die $error if $error;
507 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
512 =item generate_email PARAMHASH
514 PARAMHASH can contain the following:
518 =item from => sender address, required
520 =item tempate => alternate template name, optional
522 =item print_text => text attachment arrayref, optional
524 =item subject => email subject, optional
528 Returns an argument list to be passed to L<FS::Misc::send_email>.
539 my $me = '[FS::cust_bill::generate_email]';
542 'from' => $args{'from'},
543 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
546 if (ref($args{'to'}) eq 'ARRAY') {
547 $return{'to'} = $args{'to'};
549 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
550 $self->cust_main->invoicing_list
554 if ( $conf->exists('invoice_html') ) {
556 warn "$me creating HTML/text multipart message"
559 $return{'nobody'} = 1;
561 my $alternative = build MIME::Entity
562 'Type' => 'multipart/alternative',
563 'Encoding' => '7bit',
564 'Disposition' => 'inline'
568 if ( $conf->exists('invoice_email_pdf')
569 and scalar($conf->config('invoice_email_pdf_note')) ) {
571 warn "$me using 'invoice_email_pdf_note' in multipart message"
573 $data = [ map { $_ . "\n" }
574 $conf->config('invoice_email_pdf_note')
579 warn "$me not using 'invoice_email_pdf_note' in multipart message"
581 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
582 $data = $args{'print_text'};
584 $data = [ $self->print_text('', $args{'template'}) ];
589 $alternative->attach(
590 'Type' => 'text/plain',
591 #'Encoding' => 'quoted-printable',
592 'Encoding' => '7bit',
594 'Disposition' => 'inline',
597 $args{'from'} =~ /\@([\w\.\-]+)/;
598 my $from = $1 || 'example.com';
599 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
601 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
603 if ( defined($args{'template'}) && length($args{'template'})
604 && -e "$path/logo_". $args{'template'}. ".png"
607 $file = "$path/logo_". $args{'template'}. ".png";
609 $file = "$path/logo.png";
612 my $image = build MIME::Entity
613 'Type' => 'image/png',
614 'Encoding' => 'base64',
616 'Filename' => 'logo.png',
617 'Content-ID' => "<$content_id>",
620 $alternative->attach(
621 'Type' => 'text/html',
622 'Encoding' => 'quoted-printable',
623 'Data' => [ '<html>',
626 ' '. encode_entities($return{'subject'}),
629 ' <body bgcolor="#e8e8e8">',
630 $self->print_html('', $args{'template'}, $content_id),
634 'Disposition' => 'inline',
635 #'Filename' => 'invoice.pdf',
638 if ( $conf->exists('invoice_email_pdf') ) {
643 # multipart/alternative
649 my $related = build MIME::Entity 'Type' => 'multipart/related',
650 'Encoding' => '7bit';
652 #false laziness w/Misc::send_email
653 $related->head->replace('Content-type',
655 '; boundary="'. $related->head->multipart_boundary. '"'.
656 '; type=multipart/alternative'
659 $related->add_part($alternative);
661 $related->add_part($image);
663 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
665 $return{'mimeparts'} = [ $related, $pdf ];
669 #no other attachment:
671 # multipart/alternative
676 $return{'content-type'} = 'multipart/related';
677 $return{'mimeparts'} = [ $alternative, $image ];
678 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
679 #$return{'disposition'} = 'inline';
685 if ( $conf->exists('invoice_email_pdf') ) {
686 warn "$me creating PDF attachment"
689 #mime parts arguments a la MIME::Entity->build().
690 $return{'mimeparts'} = [
691 { $self->mimebuild_pdf('', $args{'template'}) }
695 if ( $conf->exists('invoice_email_pdf')
696 and scalar($conf->config('invoice_email_pdf_note')) ) {
698 warn "$me using 'invoice_email_pdf_note'"
700 $return{'body'} = [ map { $_ . "\n" }
701 $conf->config('invoice_email_pdf_note')
706 warn "$me not using 'invoice_email_pdf_note'"
708 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
709 $return{'body'} = $args{'print_text'};
711 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
724 Returns a list suitable for passing to MIME::Entity->build(), representing
725 this invoice as PDF attachment.
732 'Type' => 'application/pdf',
733 'Encoding' => 'base64',
734 'Data' => [ $self->print_pdf(@_) ],
735 'Disposition' => 'attachment',
736 'Filename' => 'invoice.pdf',
740 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
742 Sends this invoice to the destinations configured for this customer: sends
743 email, prints and/or faxes. See L<FS::cust_main_invoice>.
745 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
747 AGENTNUM, if specified, means that this invoice will only be sent for customers
748 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
749 single agent) or an arrayref of agentnums.
751 INVOICE_FROM, if specified, overrides the default email invoice From: address.
753 AMOUNT, if specified, only sends the invoice if the total amount owed on this
754 invoice and all older invoices is greater than the specified amount.
761 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
762 or die "invalid invoice number: " . $opt{invnum};
764 my @args = ( $opt{template}, $opt{agentnum} );
765 push @args, $opt{invoice_from}
766 if exists($opt{invoice_from}) && $opt{invoice_from};
768 my $error = $self->send( @args );
769 die $error if $error;
775 my $template = scalar(@_) ? shift : '';
776 if ( scalar(@_) && $_[0] ) {
777 my $agentnums = ref($_[0]) ? shift : [ shift ];
778 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
784 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
786 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
789 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
791 my @invoicing_list = $self->cust_main->invoicing_list;
793 $self->email($template, $invoice_from)
794 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
796 $self->print($template)
797 if grep { $_ eq 'POST' } @invoicing_list; #postal
799 $self->fax($template)
800 if grep { $_ eq 'FAX' } @invoicing_list; #fax
806 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
810 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
812 INVOICE_FROM, if specified, overrides the default email invoice From: address.
816 sub queueable_email {
819 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
820 or die "invalid invoice number: " . $opt{invnum};
822 my @args = ( $opt{template} );
823 push @args, $opt{invoice_from}
824 if exists($opt{invoice_from}) && $opt{invoice_from};
826 my $error = $self->email( @args );
827 die $error if $error;
833 my $template = scalar(@_) ? shift : '';
837 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
839 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
840 $self->cust_main->invoicing_list;
842 #better to notify this person than silence
843 @invoicing_list = ($invoice_from) unless @invoicing_list;
845 my $error = send_email(
846 $self->generate_email(
847 'from' => $invoice_from,
848 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
849 'template' => $template,
852 die "can't email invoice: $error\n" if $error;
853 #die "$error\n" if $error;
857 =item lpr_data [ TEMPLATENAME ]
859 Returns the postscript or plaintext for this invoice as an arrayref.
861 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
866 my( $self, $template) = @_;
867 $conf->exists('invoice_latex')
868 ? [ $self->print_ps('', $template) ]
869 : [ $self->print_text('', $template) ];
872 =item print [ TEMPLATENAME ]
876 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
882 my $template = scalar(@_) ? shift : '';
884 do_print $self->lpr_data($template);
887 =item fax [ TEMPLATENAME ]
891 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
897 my $template = scalar(@_) ? shift : '';
899 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
900 unless $conf->exists('invoice_latex');
902 my $dialstring = $self->cust_main->getfield('fax');
905 my $error = send_fax( 'docdata' => $self->lpr_data($template),
906 'dialstring' => $dialstring,
908 die $error if $error;
912 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
914 Like B<send>, but only sends the invoice if it is the newest open invoice for
924 grep { $_->owed > 0 }
925 qsearch('cust_bill', {
926 'custnum' => $self->custnum,
927 #'_date' => { op=>'>', value=>$self->_date },
928 'invnum' => { op=>'>', value=>$self->invnum },
935 =item send_csv OPTION => VALUE, ...
937 Sends invoice as a CSV data-file to a remote host with the specified protocol.
941 protocol - currently only "ftp"
947 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
948 and YYMMDDHHMMSS is a timestamp.
950 See L</print_csv> for a description of the output format.
955 my($self, %opt) = @_;
959 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
960 mkdir $spooldir, 0700 unless -d $spooldir;
962 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
963 my $file = "$spooldir/$tracctnum.csv";
965 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
967 open(CSV, ">$file") or die "can't open $file: $!";
975 if ( $opt{protocol} eq 'ftp' ) {
976 eval "use Net::FTP;";
978 $net = Net::FTP->new($opt{server}) or die @$;
980 die "unknown protocol: $opt{protocol}";
983 $net->login( $opt{username}, $opt{password} )
984 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
986 $net->binary or die "can't set binary mode";
988 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
990 $net->put($file) or die "can't put $file: $!";
1000 Spools CSV invoice data.
1006 =item format - 'default' or 'billco'
1008 =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>).
1010 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1012 =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.
1019 my($self, %opt) = @_;
1021 my $cust_main = $self->cust_main;
1023 if ( $opt{'dest'} ) {
1024 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1025 $cust_main->invoicing_list;
1026 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1027 || ! keys %invoicing_list;
1030 if ( $opt{'balanceover'} ) {
1032 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1035 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1036 mkdir $spooldir, 0700 unless -d $spooldir;
1038 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1042 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1043 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1046 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1048 open(CSV, ">>$file") or die "can't open $file: $!";
1049 flock(CSV, LOCK_EX);
1054 if ( lc($opt{'format'}) eq 'billco' ) {
1056 flock(CSV, LOCK_UN);
1061 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1064 open(CSV,">>$file") or die "can't open $file: $!";
1065 flock(CSV, LOCK_EX);
1071 flock(CSV, LOCK_UN);
1078 =item print_csv OPTION => VALUE, ...
1080 Returns CSV data for this invoice.
1084 format - 'default' or 'billco'
1086 Returns a list consisting of two scalars. The first is a single line of CSV
1087 header information for this invoice. The second is one or more lines of CSV
1088 detail information for this invoice.
1090 If I<format> is not specified or "default", the fields of the CSV file are as
1093 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1097 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1099 B<record_type> is C<cust_bill> for the initial header line only. The
1100 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1101 fields are filled in.
1103 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1104 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1107 =item invnum - invoice number
1109 =item custnum - customer number
1111 =item _date - invoice date
1113 =item charged - total invoice amount
1115 =item first - customer first name
1117 =item last - customer first name
1119 =item company - company name
1121 =item address1 - address line 1
1123 =item address2 - address line 1
1133 =item pkg - line item description
1135 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1137 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1139 =item sdate - start date for recurring fee
1141 =item edate - end date for recurring fee
1145 If I<format> is "billco", the fields of the header CSV file are as follows:
1147 +-------------------------------------------------------------------+
1148 | FORMAT HEADER FILE |
1149 |-------------------------------------------------------------------|
1150 | Field | Description | Name | Type | Width |
1151 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1152 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1153 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1154 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1155 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1156 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1157 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1158 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1159 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1160 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1161 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1162 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1163 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1164 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1165 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1166 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1167 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1168 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1169 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1170 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1171 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1172 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1173 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1174 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1175 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1176 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1177 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1178 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1179 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1180 +-------+-------------------------------+------------+------+-------+
1182 If I<format> is "billco", the fields of the detail CSV file are as follows:
1184 FORMAT FOR DETAIL FILE
1186 Field | Description | Name | Type | Width
1187 1 | N/A-Leave Empty | RC | CHAR | 2
1188 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1189 3 | Account Number | TRACCTNUM | CHAR | 15
1190 4 | Invoice Number | TRINVOICE | CHAR | 15
1191 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1192 6 | Transaction Detail | DETAILS | CHAR | 100
1193 7 | Amount | AMT | NUM* | 9
1194 8 | Line Format Control** | LNCTRL | CHAR | 2
1195 9 | Grouping Code | GROUP | CHAR | 2
1196 10 | User Defined | ACCT CODE | CHAR | 15
1201 my($self, %opt) = @_;
1203 eval "use Text::CSV_XS";
1206 my $cust_main = $self->cust_main;
1208 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1210 if ( lc($opt{'format'}) eq 'billco' ) {
1213 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1216 if ( $conf->exists('invoice_default_terms')
1217 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1218 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1221 my( $previous_balance, @unused ) = $self->previous; #previous balance
1223 my $pmt_cr_applied = 0;
1224 $pmt_cr_applied += $_->{'amount'}
1225 foreach ( $self->_items_payments, $self->_items_credits ) ;
1227 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1230 '', # 1 | N/A-Leave Empty CHAR 2
1231 '', # 2 | N/A-Leave Empty CHAR 15
1232 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1233 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1234 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1235 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1236 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1237 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1238 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1239 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1240 '', # 10 | Ancillary Billing Information CHAR 30
1241 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1242 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1245 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1248 $duedate, # 14 | Bill Due Date CHAR 10
1250 $previous_balance, # 15 | Previous Balance NUM* 9
1251 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1252 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1253 $totaldue, # 18 | Total Amt Due NUM* 9
1254 $totaldue, # 19 | Total Amt Due NUM* 9
1255 '', # 20 | 30 Day Aging NUM* 9
1256 '', # 21 | 60 Day Aging NUM* 9
1257 '', # 22 | 90 Day Aging NUM* 9
1258 'N', # 23 | Y/N CHAR 1
1259 '', # 24 | Remittance automation CHAR 100
1260 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1261 $self->custnum, # 26 | Customer Reference Number CHAR 15
1262 '0', # 27 | Federal Tax*** NUM* 9
1263 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1264 '0', # 29 | Other Taxes & Fees*** NUM* 9
1273 time2str("%x", $self->_date),
1274 sprintf("%.2f", $self->charged),
1275 ( map { $cust_main->getfield($_) }
1276 qw( first last company address1 address2 city state zip country ) ),
1278 ) or die "can't create csv";
1281 my $header = $csv->string. "\n";
1284 if ( lc($opt{'format'}) eq 'billco' ) {
1287 foreach my $item ( $self->_items_pkg ) {
1290 '', # 1 | N/A-Leave Empty CHAR 2
1291 '', # 2 | N/A-Leave Empty CHAR 15
1292 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1293 $self->invnum, # 4 | Invoice Number CHAR 15
1294 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1295 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1296 $item->{'amount'}, # 7 | Amount NUM* 9
1297 '', # 8 | Line Format Control** CHAR 2
1298 '', # 9 | Grouping Code CHAR 2
1299 '', # 10 | User Defined CHAR 15
1302 $detail .= $csv->string. "\n";
1308 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1310 my($pkg, $setup, $recur, $sdate, $edate);
1311 if ( $cust_bill_pkg->pkgnum ) {
1313 ($pkg, $setup, $recur, $sdate, $edate) = (
1314 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1315 ( $cust_bill_pkg->setup != 0
1316 ? sprintf("%.2f", $cust_bill_pkg->setup )
1318 ( $cust_bill_pkg->recur != 0
1319 ? sprintf("%.2f", $cust_bill_pkg->recur )
1321 ( $cust_bill_pkg->sdate
1322 ? time2str("%x", $cust_bill_pkg->sdate)
1324 ($cust_bill_pkg->edate
1325 ?time2str("%x", $cust_bill_pkg->edate)
1329 } else { #pkgnum tax
1330 next unless $cust_bill_pkg->setup != 0;
1331 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1332 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1334 ($pkg, $setup, $recur, $sdate, $edate) =
1335 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1341 ( map { '' } (1..11) ),
1342 ($pkg, $setup, $recur, $sdate, $edate)
1343 ) or die "can't create csv";
1345 $detail .= $csv->string. "\n";
1351 ( $header, $detail );
1357 Pays this invoice with a compliemntary payment. If there is an error,
1358 returns the error, otherwise returns false.
1364 my $cust_pay = new FS::cust_pay ( {
1365 'invnum' => $self->invnum,
1366 'paid' => $self->owed,
1369 'payinfo' => $self->cust_main->payinfo,
1377 Attempts to pay this invoice with a credit card payment via a
1378 Business::OnlinePayment realtime gateway. See
1379 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1380 for supported processors.
1386 $self->realtime_bop( 'CC', @_ );
1391 Attempts to pay this invoice with an electronic check (ACH) payment via a
1392 Business::OnlinePayment realtime gateway. See
1393 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1394 for supported processors.
1400 $self->realtime_bop( 'ECHECK', @_ );
1405 Attempts to pay this invoice with phone bill (LEC) payment via a
1406 Business::OnlinePayment realtime gateway. See
1407 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1408 for supported processors.
1414 $self->realtime_bop( 'LEC', @_ );
1418 my( $self, $method ) = @_;
1420 my $cust_main = $self->cust_main;
1421 my $balance = $cust_main->balance;
1422 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1423 $amount = sprintf("%.2f", $amount);
1424 return "not run (balance $balance)" unless $amount > 0;
1426 my $description = 'Internet Services';
1427 if ( $conf->exists('business-onlinepayment-description') ) {
1428 my $dtempl = $conf->config('business-onlinepayment-description');
1430 my $agent_obj = $cust_main->agent
1431 or die "can't retreive agent for $cust_main (agentnum ".
1432 $cust_main->agentnum. ")";
1433 my $agent = $agent_obj->agent;
1434 my $pkgs = join(', ',
1435 map { $_->cust_pkg->part_pkg->pkg }
1436 grep { $_->pkgnum } $self->cust_bill_pkg
1438 $description = eval qq("$dtempl");
1441 $cust_main->realtime_bop($method, $amount,
1442 'description' => $description,
1443 'invnum' => $self->invnum,
1448 =item batch_card OPTION => VALUE...
1450 Adds a payment for this invoice to the pending credit card batch (see
1451 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1452 runs the payment using a realtime gateway.
1457 my ($self, %options) = @_;
1458 my $cust_main = $self->cust_main;
1460 $options{invnum} = $self->invnum;
1462 $cust_main->batch_card(%options);
1465 sub _agent_template {
1467 $self->cust_main->agent_template;
1470 sub _agent_invoice_from {
1472 $self->cust_main->agent_invoice_from;
1475 =item print_text [ TIME [ , TEMPLATE ] ]
1477 Returns an text invoice, as a list of lines.
1479 TIME an optional value used to control the printing of overdue messages. The
1480 default is now. It isn't the date of the invoice; that's the `_date' field.
1481 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1482 L<Time::Local> and L<Date::Parse> for conversion functions.
1486 #still some false laziness w/_items stuff (and send_csv)
1489 my( $self, $today, $template ) = @_;
1492 # my $invnum = $self->invnum;
1493 my $cust_main = $self->cust_main;
1494 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1495 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1497 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1498 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1499 #my $balance_due = $self->owed + $pr_total - $cr_total;
1500 my $balance_due = $self->owed + $pr_total;
1503 #my($description,$amount);
1507 unless ($conf->exists('disable_previous_balance')) {
1508 foreach ( @pr_cust_bill ) {
1510 "Previous Balance, Invoice #". $_->invnum.
1511 " (". time2str("%x",$_->_date). ")",
1512 $money_char. sprintf("%10.2f",$_->owed)
1515 if (@pr_cust_bill) {
1516 push @buf,['','-----------'];
1517 push @buf,[ 'Total Previous Balance',
1518 $money_char. sprintf("%10.2f",$pr_total ) ];
1524 foreach my $cust_bill_pkg (
1525 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1526 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1529 my $desc = $cust_bill_pkg->desc;
1531 if ( $cust_bill_pkg->pkgnum > 0 ) {
1533 if ( $cust_bill_pkg->setup != 0 ) {
1534 my $description = $desc;
1535 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1536 push @buf, [ $description,
1537 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1539 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1540 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1543 if ( $cust_bill_pkg->recur != 0 ) {
1546 ( $conf->exists('disable_line_item_date_ranges')
1548 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1549 time2str("%x", $cust_bill_pkg->edate) . ")"
1551 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1554 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1555 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1556 $cust_bill_pkg->sdate );
1559 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1561 } else { #pkgnum tax or one-shot line item
1563 if ( $cust_bill_pkg->setup != 0 ) {
1565 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1567 if ( $cust_bill_pkg->recur != 0 ) {
1568 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1569 . time2str("%x", $cust_bill_pkg->edate). ")",
1570 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1578 push @buf,['','-----------'];
1579 push @buf,[ ( $conf->exists('disable_previous_balance')
1581 : 'Total New Charges'),
1582 $money_char. sprintf("%10.2f",$self->charged) ];
1585 unless ($conf->exists('disable_previous_balance')) {
1586 push @buf,['','-----------'];
1587 push @buf,['Total Charges',
1588 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1592 foreach ( $self->cust_credited ) {
1594 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1596 my $reason = substr($_->cust_credit->reason,0,32);
1597 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1598 $reason = " ($reason) " if $reason;
1600 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1602 $money_char. sprintf("%10.2f",$_->amount)
1605 #foreach ( @cr_cust_credit ) {
1607 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1608 # $money_char. sprintf("%10.2f",$_->credited)
1612 #get & print payments
1613 foreach ( $self->cust_bill_pay ) {
1615 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1618 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1619 $money_char. sprintf("%10.2f",$_->amount )
1624 my $balance_due_msg = $self->balance_due_msg;
1626 push @buf,['','-----------'];
1627 push @buf,[$balance_due_msg, $money_char.
1628 sprintf("%10.2f", $balance_due ) ];
1631 #create the template
1632 $template ||= $self->_agent_template;
1633 my $templatefile = 'invoice_template';
1634 $templatefile .= "_$template" if length($template);
1635 my @invoice_template = $conf->config($templatefile)
1636 or die "cannot load config file $templatefile";
1639 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1640 /invoice_lines\((\d*)\)/;
1641 $invoice_lines += $1 || scalar(@buf);
1644 die "no invoice_lines() functions in template?" unless $wasfunc;
1645 my $invoice_template = new Text::Template (
1647 SOURCE => [ map "$_\n", @invoice_template ],
1648 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1649 $invoice_template->compile()
1650 or die "can't compile template: $Text::Template::ERROR";
1652 #setup template variables
1653 package FS::cust_bill::_template; #!
1654 use vars qw( $custnum $invnum $date $agent @address $overdue
1655 $page $total_pages @buf );
1657 $custnum = $self->custnum;
1658 $invnum = $self->invnum;
1659 $date = $self->_date;
1660 $agent = $self->cust_main->agent->agent;
1663 if ( $FS::cust_bill::invoice_lines ) {
1665 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1667 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1672 #format address (variable for the template)
1674 @address = ( '', '', '', '', '', '' );
1675 package FS::cust_bill; #!
1676 $FS::cust_bill::_template::address[$l++] =
1677 $cust_main->payname.
1678 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1679 ? " (P.O. #". $cust_main->payinfo. ")"
1683 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1684 if $cust_main->company;
1685 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1686 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1687 if $cust_main->address2;
1688 $FS::cust_bill::_template::address[$l++] =
1689 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1691 my $countrydefault = $conf->config('countrydefault') || 'US';
1692 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1693 unless $cust_main->country eq $countrydefault;
1695 # #overdue? (variable for the template)
1696 # $FS::cust_bill::_template::overdue = (
1698 # && $today > $self->_date
1699 ## && $self->printed > 1
1700 # && $self->printed > 0
1703 #and subroutine for the template
1704 sub FS::cust_bill::_template::invoice_lines {
1705 my $lines = shift || scalar(@buf);
1707 scalar(@buf) ? shift @buf : [ '', '' ];
1713 $FS::cust_bill::_template::page = 1;
1717 push @collect, split("\n",
1718 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1720 $FS::cust_bill::_template::page++;
1723 map "$_\n", @collect;
1727 =item print_latex [ TIME [ , TEMPLATE ] ]
1729 Internal method - returns a filename of a filled-in LaTeX template for this
1730 invoice (Note: add ".tex" to get the actual filename).
1732 See print_ps and print_pdf for methods that return PostScript and PDF output.
1734 TIME an optional value used to control the printing of overdue messages. The
1735 default is now. It isn't the date of the invoice; that's the `_date' field.
1736 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1737 L<Time::Local> and L<Date::Parse> for conversion functions.
1741 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1744 my( $self, $today, $template ) = @_;
1746 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1749 my $cust_main = $self->cust_main;
1750 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1751 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1753 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1754 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1755 #my $balance_due = $self->owed + $pr_total - $cr_total;
1756 my $balance_due = $self->owed + $pr_total;
1758 #create the template
1759 $template ||= $self->_agent_template;
1760 my $templatefile = 'invoice_latex';
1761 my $suffix = length($template) ? "_$template" : '';
1762 $templatefile .= $suffix;
1763 my @invoice_template = map "$_\n", $conf->config($templatefile)
1764 or die "cannot load config file $templatefile";
1766 my($format, $text_template);
1767 if ( grep { /^%%Detail/ } @invoice_template ) {
1768 #change this to a die when the old code is removed
1769 warn "old-style invoice template $templatefile; ".
1770 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1773 $format = 'Text::Template';
1774 $text_template = new Text::Template(
1776 SOURCE => \@invoice_template,
1777 DELIMITERS => [ '[@--', '--@]' ],
1780 $text_template->compile()
1781 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1785 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1786 $returnaddress = join("\n",
1787 $conf->config_orbase('invoice_latexreturnaddress', $template)
1790 $returnaddress = '~';
1793 my %invoice_data = (
1794 'custnum' => $self->custnum,
1795 'invnum' => $self->invnum,
1796 'date' => time2str('%b %o, %Y', $self->_date),
1797 'today' => time2str('%b %o, %Y', $today),
1798 'agent' => _latex_escape($cust_main->agent->agent),
1799 'agent_custid' => _latex_escape($cust_main->agent_custid),
1800 'payname' => _latex_escape($cust_main->payname),
1801 'company' => _latex_escape($cust_main->company),
1802 'address1' => _latex_escape($cust_main->address1),
1803 'address2' => _latex_escape($cust_main->address2),
1804 'city' => _latex_escape($cust_main->city),
1805 'state' => _latex_escape($cust_main->state),
1807 'zip' => _latex_escape($cust_main->zip),
1808 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1809 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1810 'returnaddress' => $returnaddress,
1812 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1813 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1814 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1815 'balance' => $balance_due,
1816 'ship_enable' => $conf->exists('invoice-ship_address'),
1817 'unitprices' => $conf->exists('invoice-unitprice'),
1820 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1821 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1822 my $method = $prefix.$_;
1823 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1826 my $countrydefault = $conf->config('countrydefault') || 'US';
1827 if ( $cust_main->country eq $countrydefault ) {
1828 $invoice_data{'country'} = '';
1830 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1833 $invoice_data{'notes'} =
1835 # #do variable substitutions in notes
1836 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1837 $conf->config_orbase('invoice_latexnotes', $template)
1839 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1842 #do variable substitution in coupon
1843 foreach my $include (qw( coupon )) {
1845 my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1847 my $inc_tt = new Text::Template (
1849 SOURCE => [ map "$_\n", @inc_src ],
1850 DELIMITERS => [ '[@--', '--@]' ],
1851 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1853 unless ( $inc_tt->compile() ) {
1854 my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1855 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1859 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1861 $invoice_data{$include} =~ s/\n+$//
1864 $invoice_data{'footer'} =~ s/\n+$//;
1865 $invoice_data{'smallfooter'} =~ s/\n+$//;
1866 $invoice_data{'notes'} =~ s/\n+$//;
1868 $invoice_data{'po_line'} =
1869 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1870 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1874 if ( $format eq 'old' ) {
1877 my @total_item = ();
1878 while ( @invoice_template ) {
1879 my $line = shift @invoice_template;
1881 if ( $line =~ /^%%Detail\s*$/ ) {
1883 while ( ( my $line_item_line = shift @invoice_template )
1884 !~ /^%%EndDetail\s*$/ ) {
1885 push @line_item, $line_item_line;
1887 foreach my $line_item ( $self->_items ) {
1888 #foreach my $line_item ( $self->_items_pkg ) {
1889 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1890 $invoice_data{'description'} =
1891 _latex_escape($line_item->{'description'});
1892 if ( exists $line_item->{'ext_description'} ) {
1893 $invoice_data{'description'} .=
1894 "\\tabularnewline\n~~".
1895 join( "\\tabularnewline\n~~",
1896 map _latex_escape($_), @{$line_item->{'ext_description'}}
1899 $invoice_data{'amount'} = $line_item->{'amount'};
1900 $invoice_data{'unit_amount'} = $line_item->{'unit_amount'};
1901 $invoice_data{'quantity'} = $line_item->{'quantity'};
1902 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1904 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1907 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1909 while ( ( my $total_item_line = shift @invoice_template )
1910 !~ /^%%EndTotalDetails\s*$/ ) {
1911 push @total_item, $total_item_line;
1914 my @total_fill = ();
1917 foreach my $tax ( $self->_items_tax ) {
1918 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1919 $taxtotal += $tax->{'amount'};
1920 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1922 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1927 $invoice_data{'total_item'} = 'Sub-total';
1928 $invoice_data{'total_amount'} =
1929 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1930 unshift @total_fill,
1931 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1935 $invoice_data{'total_item'} = '\textbf{Total}';
1936 $invoice_data{'total_amount'} =
1937 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1939 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1942 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1945 foreach my $credit ( $self->_items_credits ) {
1946 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1948 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1950 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1955 foreach my $payment ( $self->_items_payments ) {
1956 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1958 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1960 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1964 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1965 $invoice_data{'total_amount'} =
1966 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1968 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1971 push @filled_in, @total_fill;
1974 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1975 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1976 push @filled_in, $line;
1987 } elsif ( $format eq 'Text::Template' ) {
1989 my @detail_items = ();
1990 my @total_items = ();
1992 $invoice_data{'detail_items'} = \@detail_items;
1993 $invoice_data{'total_items'} = \@total_items;
1995 my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
1996 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
1998 ext_description => [],
2000 $detail->{'ref'} = $line_item->{'pkgnum'};
2001 $detail->{'quantity'} = 1;
2002 $detail->{'description'} = _latex_escape($line_item->{'description'});
2003 if ( exists $line_item->{'ext_description'} ) {
2004 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2006 $detail->{'amount'} = $line_item->{'amount'};
2007 $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2008 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2010 push @detail_items, $detail;
2015 foreach my $tax ( $self->_items_tax ) {
2017 $total->{'total_item'} = _latex_escape($tax->{'description'});
2018 $taxtotal += $tax->{'amount'};
2019 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2020 push @total_items, $total;
2025 $total->{'total_item'} = 'Sub-total';
2026 $total->{'total_amount'} =
2027 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2028 unshift @total_items, $total;
2033 $total->{'total_item'} = '\textbf{Total}';
2034 $total->{'total_amount'} =
2037 $self->charged + ( $conf->exists('disable_previous_balance')
2043 push @total_items, $total;
2046 unless ($conf->exists('disable_previous_balance')) {
2047 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2050 foreach my $credit ( $self->_items_credits ) {
2052 $total->{'total_item'} = _latex_escape($credit->{'description'});
2054 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2055 push @total_items, $total;
2059 foreach my $payment ( $self->_items_payments ) {
2061 $total->{'total_item'} = _latex_escape($payment->{'description'});
2063 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2064 push @total_items, $total;
2069 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2070 $total->{'total_amount'} =
2071 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2072 push @total_items, $total;
2077 die "guru meditation #54";
2080 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2081 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2085 ) or die "can't open temp file: $!\n";
2086 if ( $format eq 'old' ) {
2087 print $fh join('', @filled_in );
2088 } elsif ( $format eq 'Text::Template' ) {
2089 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2091 die "guru meditation #32";
2095 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2100 =item print_ps [ TIME [ , TEMPLATE ] ]
2102 Returns an postscript invoice, as a scalar.
2104 TIME an optional value used to control the printing of overdue messages. The
2105 default is now. It isn't the date of the invoice; that's the `_date' field.
2106 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2107 L<Time::Local> and L<Date::Parse> for conversion functions.
2114 my $file = $self->print_latex(@_);
2115 my $ps = generate_ps($file);
2120 =item print_pdf [ TIME [ , TEMPLATE ] ]
2122 Returns an PDF invoice, as a scalar.
2124 TIME an optional value used to control the printing of overdue messages. The
2125 default is now. It isn't the date of the invoice; that's the `_date' field.
2126 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2127 L<Time::Local> and L<Date::Parse> for conversion functions.
2134 my $file = $self->print_latex(@_);
2135 my $pdf = generate_pdf($file);
2140 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2142 Returns an HTML invoice, as a scalar.
2144 TIME an optional value used to control the printing of overdue messages. The
2145 default is now. It isn't the date of the invoice; that's the `_date' field.
2146 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2147 L<Time::Local> and L<Date::Parse> for conversion functions.
2149 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2150 when emailing the invoice as part of a multipart/related MIME email.
2154 #some falze laziness w/print_text and print_latex (and send_csv)
2156 my( $self, $today, $template, $cid ) = @_;
2159 my $cust_main = $self->cust_main;
2160 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2161 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2163 $template ||= $self->_agent_template;
2164 my $templatefile = 'invoice_html';
2165 my $suffix = length($template) ? "_$template" : '';
2166 $templatefile .= $suffix;
2167 my @html_template = map "$_\n", $conf->config($templatefile)
2168 or die "cannot load config file $templatefile";
2170 my $html_template = new Text::Template(
2172 SOURCE => \@html_template,
2173 DELIMITERS => [ '<%=', '%>' ],
2176 $html_template->compile()
2177 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2179 my %invoice_data = (
2180 'custnum' => $self->custnum,
2181 'invnum' => $self->invnum,
2182 'date' => time2str('%b %o, %Y', $self->_date),
2183 'today' => time2str('%b %o, %Y', $today),
2184 'agent' => encode_entities($cust_main->agent->agent),
2185 'agent_custid' => encode_entities($cust_main->agent_custid),
2186 'payname' => encode_entities($cust_main->payname),
2187 'company' => encode_entities($cust_main->company),
2188 'address1' => encode_entities($cust_main->address1),
2189 'address2' => encode_entities($cust_main->address2),
2190 'city' => encode_entities($cust_main->city),
2191 'state' => encode_entities($cust_main->state),
2192 'zip' => encode_entities($cust_main->zip),
2193 'terms' => $conf->config('invoice_default_terms')
2194 || 'Payable upon receipt',
2196 'template' => $template,
2197 'ship_enable' => $conf->exists('invoice-ship_address'),
2198 'unitprices' => $conf->exists('invoice-unitprice'),
2199 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2202 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2203 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2204 my $method = $prefix.$_;
2205 $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2209 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2210 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2212 $invoice_data{'returnaddress'} =
2213 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2215 $invoice_data{'returnaddress'} =
2218 s/\\\\\*?\s*$/<BR>/;
2219 s/\\hyphenation\{[\w\s\-]+\}//;
2222 $conf->config_orbase( 'invoice_latexreturnaddress',
2228 my $countrydefault = $conf->config('countrydefault') || 'US';
2229 if ( $cust_main->country eq $countrydefault ) {
2230 $invoice_data{'country'} = '';
2232 $invoice_data{'country'} =
2233 encode_entities(code2country($cust_main->country));
2237 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2238 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2240 $invoice_data{'notes'} =
2241 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2243 $invoice_data{'notes'} =
2245 s/%%(.*)$/<!-- $1 -->/g;
2246 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2247 s/\\begin\{enumerate\}/<ol>/g;
2249 s/\\end\{enumerate\}/<\/ol>/g;
2250 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2257 $conf->config_orbase('invoice_latexnotes', $template)
2261 # #do variable substitutions in notes
2262 # $invoice_data{'notes'} =
2264 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2265 # $conf->config_orbase('invoice_latexnotes', $suffix)
2269 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2270 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2272 $invoice_data{'footer'} =
2273 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2275 $invoice_data{'footer'} =
2276 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2277 $conf->config_orbase('invoice_latexfooter', $template)
2281 $invoice_data{'po_line'} =
2282 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2283 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2286 my $money_char = $conf->config('money_char') || '$';
2288 my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2289 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2291 ext_description => [],
2293 $detail->{'ref'} = $line_item->{'pkgnum'};
2294 $detail->{'description'} = encode_entities($line_item->{'description'});
2295 if ( exists $line_item->{'ext_description'} ) {
2296 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2298 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2299 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2301 push @{$invoice_data{'detail_items'}}, $detail;
2306 foreach my $tax ( $self->_items_tax ) {
2308 $total->{'total_item'} = encode_entities($tax->{'description'});
2309 $taxtotal += $tax->{'amount'};
2310 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2311 push @{$invoice_data{'total_items'}}, $total;
2316 $total->{'total_item'} = 'Sub-total';
2317 $total->{'total_amount'} =
2318 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2319 unshift @{$invoice_data{'total_items'}}, $total;
2322 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2325 $total->{'total_item'} = '<b>Total</b>';
2326 $total->{'total_amount'} =
2329 $self->charged + ( $conf->exists('disable_previous_balance')
2335 push @{$invoice_data{'total_items'}}, $total;
2338 unless ($conf->exists('disable_previous_balance')) {
2339 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2342 foreach my $credit ( $self->_items_credits ) {
2344 $total->{'total_item'} = encode_entities($credit->{'description'});
2346 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2347 push @{$invoice_data{'total_items'}}, $total;
2351 foreach my $payment ( $self->_items_payments ) {
2353 $total->{'total_item'} = encode_entities($payment->{'description'});
2355 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2356 push @{$invoice_data{'total_items'}}, $total;
2361 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2362 $total->{'total_amount'} =
2363 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2364 push @{$invoice_data{'total_items'}}, $total;
2368 $html_template->fill_in( HASH => \%invoice_data);
2371 # quick subroutine for print_latex
2373 # There are ten characters that LaTeX treats as special characters, which
2374 # means that they do not simply typeset themselves:
2375 # # $ % & ~ _ ^ \ { }
2377 # TeX ignores blanks following an escaped character; if you want a blank (as
2378 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2382 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2383 $value =~ s/([<>])/\$$1\$/g;
2387 #utility methods for print_*
2389 sub balance_due_msg {
2391 my $msg = 'Balance Due';
2392 return $msg unless $conf->exists('invoice_default_terms');
2393 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2394 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2395 } elsif ( $conf->config('invoice_default_terms') ) {
2396 $msg .= ' - '. $conf->config('invoice_default_terms');
2401 =item invnum_date_pretty
2403 Returns a string with the invoice number and date, for example:
2404 "Invoice #54 (3/20/2008)"
2408 sub invnum_date_pretty {
2410 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2416 #my @display = scalar(@_)
2418 # : qw( _items_previous _items_pkg );
2419 # #: qw( _items_pkg );
2420 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2421 my @display = qw( _items_previous _items_pkg );
2424 foreach my $display ( @display ) {
2425 push @b, $self->$display(@_);
2430 sub _items_previous {
2432 my $cust_main = $self->cust_main;
2433 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2435 foreach ( @pr_cust_bill ) {
2437 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2438 ' ('. time2str('%x',$_->_date). ')',
2439 #'pkgpart' => 'N/A',
2441 'amount' => sprintf("%.2f", $_->owed),
2447 # 'description' => 'Previous Balance',
2448 # #'pkgpart' => 'N/A',
2449 # 'pkgnum' => 'N/A',
2450 # 'amount' => sprintf("%10.2f", $pr_total ),
2451 # 'ext_description' => [ map {
2452 # "Invoice ". $_->invnum.
2453 # " (". time2str("%x",$_->_date). ") ".
2454 # sprintf("%10.2f", $_->owed)
2455 # } @pr_cust_bill ],
2462 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2463 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2468 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2469 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2472 sub _items_cust_bill_pkg {
2474 my $cust_bill_pkg = shift;
2477 my $format = $opt{format} || '';
2478 my $escape_function = $opt{escape_function} || sub { shift };
2481 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2483 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2485 my $desc = $cust_bill_pkg->desc;
2487 my %details_opt = ( 'format' => $format,
2488 'escape_function' => $escape_function,
2491 if ( $cust_bill_pkg->pkgnum > 0 ) {
2493 if ( $cust_bill_pkg->setup != 0 ) {
2495 my $description = $desc;
2496 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2498 my @d = map &{$escape_function}($_),
2499 $cust_pkg->h_labels_short($self->_date);
2500 push @d, $cust_bill_pkg->details(%details_opt)
2501 if $cust_bill_pkg->recur == 0;
2504 description => $description,
2505 #pkgpart => $part_pkg->pkgpart,
2506 pkgnum => $cust_bill_pkg->pkgnum,
2507 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2508 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2509 quantity => $cust_bill_pkg->quantity,
2510 ext_description => \@d,
2514 if ( $cust_bill_pkg->recur != 0 ) {
2516 my $description = $desc;
2517 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2518 $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2519 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2522 #at least until cust_bill_pkg has "past" ranges in addition to
2523 #the "future" sdate/edate ones... see #3032
2524 my @d = map &{$escape_function}($_),
2525 $cust_pkg->h_labels_short($self->_date);
2526 #$cust_bill_pkg->edate,
2527 #$cust_bill_pkg->sdate),
2528 push @d, $cust_bill_pkg->details(%details_opt);
2531 description => $description,
2532 #pkgpart => $part_pkg->pkgpart,
2533 pkgnum => $cust_bill_pkg->pkgnum,
2534 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2535 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2536 quantity => $cust_bill_pkg->quantity,
2537 ext_description => \@d,
2542 } else { #pkgnum tax or one-shot line item (??)
2544 if ( $cust_bill_pkg->setup != 0 ) {
2546 'description' => $desc,
2547 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2550 if ( $cust_bill_pkg->recur != 0 ) {
2552 'description' => "$desc (".
2553 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2554 time2str("%x", $cust_bill_pkg->edate). ')',
2555 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2567 sub _items_credits {
2572 foreach ( $self->cust_credited ) {
2574 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2576 my $reason = $_->cust_credit->reason;
2577 #my $reason = substr($_->cust_credit->reason,0,32);
2578 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2579 $reason = " ($reason) " if $reason;
2581 #'description' => 'Credit ref\#'. $_->crednum.
2582 # " (". time2str("%x",$_->cust_credit->_date) .")".
2584 'description' => 'Credit applied '.
2585 time2str("%x",$_->cust_credit->_date). $reason,
2586 'amount' => sprintf("%.2f",$_->amount),
2589 #foreach ( @cr_cust_credit ) {
2591 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2592 # $money_char. sprintf("%10.2f",$_->credited)
2600 sub _items_payments {
2604 #get & print payments
2605 foreach ( $self->cust_bill_pay ) {
2607 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2610 'description' => "Payment received ".
2611 time2str("%x",$_->cust_pay->_date ),
2612 'amount' => sprintf("%.2f", $_->amount )
2631 sub process_reprint {
2632 process_re_X('print', @_);
2639 sub process_reemail {
2640 process_re_X('email', @_);
2648 process_re_X('fax', @_);
2651 use Storable qw(thaw);
2655 my( $method, $job ) = ( shift, shift );
2656 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2658 my $param = thaw(decode_base64(shift));
2659 warn Dumper($param) if $DEBUG;
2670 my($method, $job, %param ) = @_;
2672 warn "re_X $method for job $job with param:\n".
2673 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2676 #some false laziness w/search/cust_bill.html
2678 my $orderby = 'ORDER BY cust_bill._date';
2680 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2682 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2684 my @cust_bill = qsearch( {
2685 #'select' => "cust_bill.*",
2686 'table' => 'cust_bill',
2687 'addl_from' => $addl_from,
2689 'extra_sql' => $extra_sql,
2690 'order_by' => $orderby,
2694 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2697 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2698 foreach my $cust_bill ( @cust_bill ) {
2699 $cust_bill->$method();
2701 if ( $job ) { #progressbar foo
2703 if ( time - $min_sec > $last ) {
2704 my $error = $job->update_statustext(
2705 int( 100 * $num / scalar(@cust_bill) )
2707 die $error if $error;
2718 =head1 CLASS METHODS
2724 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2730 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2735 Returns an SQL fragment to retreive the net amount (charged minus credited).
2741 'charged - '. $class->credited_sql;
2746 Returns an SQL fragment to retreive the amount paid against this invoice.
2752 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2753 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2758 Returns an SQL fragment to retreive the amount credited against this invoice.
2764 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2765 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2768 =item search_sql HASHREF
2770 Class method which returns an SQL WHERE fragment to search for parameters
2771 specified in HASHREF. Valid parameters are
2777 Epoch date (UNIX timestamp) setting a lower bound for _date values
2781 Epoch date (UNIX timestamp) setting an upper bound for _date values
2795 =item newest_percust
2799 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2804 my($class, $param) = @_;
2806 warn "$me search_sql called with params: \n".
2807 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2812 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2813 push @search, "cust_bill._date >= $1";
2815 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2816 push @search, "cust_bill._date < $1";
2818 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2819 push @search, "cust_bill.invnum >= $1";
2821 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2822 push @search, "cust_bill.invnum <= $1";
2824 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2825 push @search, "cust_main.agentnum = $1";
2828 push @search, '0 != '. FS::cust_bill->owed_sql
2829 if $param->{'open'};
2831 push @search, '0 != '. FS::cust_bill->net_sql
2834 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2835 if $param->{'days'};
2837 if ( $param->{'newest_percust'} ) {
2839 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2840 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2842 my @newest_where = map { my $x = $_;
2843 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2846 grep ! /^cust_main./, @search;
2847 my $newest_where = scalar(@newest_where)
2848 ? ' AND '. join(' AND ', @newest_where)
2852 push @search, "cust_bill._date = (
2853 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2854 WHERE newest_cust_bill.custnum = cust_bill.custnum
2860 my $curuser = $FS::CurrentUser::CurrentUser;
2861 if ( $curuser->username eq 'fs_queue'
2862 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2864 my $newuser = qsearchs('access_user', {
2865 'username' => $username,
2869 $curuser = $newuser;
2871 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2875 push @search, $curuser->agentnums_sql;
2877 join(' AND ', @search );
2889 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2890 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base