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_invoice($template, $invoice_from)
794 $self->email($template, $invoice_from)
795 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
797 #$self->print_invoice($template)
798 $self->print($template)
799 if grep { $_ eq 'POST' } @invoicing_list; #postal
801 $self->fax_invoice($template)
802 if grep { $_ eq 'FAX' } @invoicing_list; #fax
808 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
812 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
814 INVOICE_FROM, if specified, overrides the default email invoice From: address.
818 sub queueable_email {
821 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
822 or die "invalid invoice number: " . $opt{invnum};
824 my @args = ( $opt{template} );
825 push @args, $opt{invoice_from}
826 if exists($opt{invoice_from}) && $opt{invoice_from};
828 my $error = $self->email( @args );
829 die $error if $error;
836 my $template = scalar(@_) ? shift : '';
840 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
842 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
843 $self->cust_main->invoicing_list;
845 #better to notify this person than silence
846 @invoicing_list = ($invoice_from) unless @invoicing_list;
848 my $error = send_email(
849 $self->generate_email(
850 'from' => $invoice_from,
851 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
852 'template' => $template,
855 die "can't email invoice: $error\n" if $error;
856 #die "$error\n" if $error;
860 =item lpr_data [ TEMPLATENAME ]
862 Returns the postscript or plaintext for this invoice as an arrayref.
864 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
869 my( $self, $template) = @_;
870 $conf->exists('invoice_latex')
871 ? [ $self->print_ps('', $template) ]
872 : [ $self->print_text('', $template) ];
875 =item print [ TEMPLATENAME ]
879 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
886 my $template = scalar(@_) ? shift : '';
888 do_print $self->lpr_data($template);
891 =item fax_invoice [ TEMPLATENAME ]
895 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
901 my $template = scalar(@_) ? shift : '';
903 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
904 unless $conf->exists('invoice_latex');
906 my $dialstring = $self->cust_main->getfield('fax');
909 my $error = send_fax( 'docdata' => $self->lpr_data($template),
910 'dialstring' => $dialstring,
912 die $error if $error;
916 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
918 Like B<send>, but only sends the invoice if it is the newest open invoice for
928 grep { $_->owed > 0 }
929 qsearch('cust_bill', {
930 'custnum' => $self->custnum,
931 #'_date' => { op=>'>', value=>$self->_date },
932 'invnum' => { op=>'>', value=>$self->invnum },
939 =item send_csv OPTION => VALUE, ...
941 Sends invoice as a CSV data-file to a remote host with the specified protocol.
945 protocol - currently only "ftp"
951 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
952 and YYMMDDHHMMSS is a timestamp.
954 See L</print_csv> for a description of the output format.
959 my($self, %opt) = @_;
963 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
964 mkdir $spooldir, 0700 unless -d $spooldir;
966 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
967 my $file = "$spooldir/$tracctnum.csv";
969 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
971 open(CSV, ">$file") or die "can't open $file: $!";
979 if ( $opt{protocol} eq 'ftp' ) {
980 eval "use Net::FTP;";
982 $net = Net::FTP->new($opt{server}) or die @$;
984 die "unknown protocol: $opt{protocol}";
987 $net->login( $opt{username}, $opt{password} )
988 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
990 $net->binary or die "can't set binary mode";
992 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
994 $net->put($file) or die "can't put $file: $!";
1004 Spools CSV invoice data.
1010 =item format - 'default' or 'billco'
1012 =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>).
1014 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1016 =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.
1023 my($self, %opt) = @_;
1025 my $cust_main = $self->cust_main;
1027 if ( $opt{'dest'} ) {
1028 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1029 $cust_main->invoicing_list;
1030 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1031 || ! keys %invoicing_list;
1034 if ( $opt{'balanceover'} ) {
1036 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1039 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1040 mkdir $spooldir, 0700 unless -d $spooldir;
1042 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1046 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1047 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1050 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1052 open(CSV, ">>$file") or die "can't open $file: $!";
1053 flock(CSV, LOCK_EX);
1058 if ( lc($opt{'format'}) eq 'billco' ) {
1060 flock(CSV, LOCK_UN);
1065 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1068 open(CSV,">>$file") or die "can't open $file: $!";
1069 flock(CSV, LOCK_EX);
1075 flock(CSV, LOCK_UN);
1082 =item print_csv OPTION => VALUE, ...
1084 Returns CSV data for this invoice.
1088 format - 'default' or 'billco'
1090 Returns a list consisting of two scalars. The first is a single line of CSV
1091 header information for this invoice. The second is one or more lines of CSV
1092 detail information for this invoice.
1094 If I<format> is not specified or "default", the fields of the CSV file are as
1097 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1101 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1103 B<record_type> is C<cust_bill> for the initial header line only. The
1104 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1105 fields are filled in.
1107 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1108 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1111 =item invnum - invoice number
1113 =item custnum - customer number
1115 =item _date - invoice date
1117 =item charged - total invoice amount
1119 =item first - customer first name
1121 =item last - customer first name
1123 =item company - company name
1125 =item address1 - address line 1
1127 =item address2 - address line 1
1137 =item pkg - line item description
1139 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1141 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1143 =item sdate - start date for recurring fee
1145 =item edate - end date for recurring fee
1149 If I<format> is "billco", the fields of the header CSV file are as follows:
1151 +-------------------------------------------------------------------+
1152 | FORMAT HEADER FILE |
1153 |-------------------------------------------------------------------|
1154 | Field | Description | Name | Type | Width |
1155 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1156 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1157 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1158 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1159 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1160 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1161 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1162 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1163 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1164 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1165 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1166 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1167 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1168 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1169 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1170 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1171 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1172 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1173 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1174 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1175 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1176 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1177 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1178 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1179 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1180 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1181 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1182 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1183 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1184 +-------+-------------------------------+------------+------+-------+
1186 If I<format> is "billco", the fields of the detail CSV file are as follows:
1188 FORMAT FOR DETAIL FILE
1190 Field | Description | Name | Type | Width
1191 1 | N/A-Leave Empty | RC | CHAR | 2
1192 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1193 3 | Account Number | TRACCTNUM | CHAR | 15
1194 4 | Invoice Number | TRINVOICE | CHAR | 15
1195 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1196 6 | Transaction Detail | DETAILS | CHAR | 100
1197 7 | Amount | AMT | NUM* | 9
1198 8 | Line Format Control** | LNCTRL | CHAR | 2
1199 9 | Grouping Code | GROUP | CHAR | 2
1200 10 | User Defined | ACCT CODE | CHAR | 15
1205 my($self, %opt) = @_;
1207 eval "use Text::CSV_XS";
1210 my $cust_main = $self->cust_main;
1212 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1214 if ( lc($opt{'format'}) eq 'billco' ) {
1217 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1220 if ( $conf->exists('invoice_default_terms')
1221 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1222 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1225 my( $previous_balance, @unused ) = $self->previous; #previous balance
1227 my $pmt_cr_applied = 0;
1228 $pmt_cr_applied += $_->{'amount'}
1229 foreach ( $self->_items_payments, $self->_items_credits ) ;
1231 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1234 '', # 1 | N/A-Leave Empty CHAR 2
1235 '', # 2 | N/A-Leave Empty CHAR 15
1236 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1237 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1238 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1239 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1240 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1241 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1242 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1243 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1244 '', # 10 | Ancillary Billing Information CHAR 30
1245 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1246 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1249 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1252 $duedate, # 14 | Bill Due Date CHAR 10
1254 $previous_balance, # 15 | Previous Balance NUM* 9
1255 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1256 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1257 $totaldue, # 18 | Total Amt Due NUM* 9
1258 $totaldue, # 19 | Total Amt Due NUM* 9
1259 '', # 20 | 30 Day Aging NUM* 9
1260 '', # 21 | 60 Day Aging NUM* 9
1261 '', # 22 | 90 Day Aging NUM* 9
1262 'N', # 23 | Y/N CHAR 1
1263 '', # 24 | Remittance automation CHAR 100
1264 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1265 $self->custnum, # 26 | Customer Reference Number CHAR 15
1266 '0', # 27 | Federal Tax*** NUM* 9
1267 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1268 '0', # 29 | Other Taxes & Fees*** NUM* 9
1277 time2str("%x", $self->_date),
1278 sprintf("%.2f", $self->charged),
1279 ( map { $cust_main->getfield($_) }
1280 qw( first last company address1 address2 city state zip country ) ),
1282 ) or die "can't create csv";
1285 my $header = $csv->string. "\n";
1288 if ( lc($opt{'format'}) eq 'billco' ) {
1291 foreach my $item ( $self->_items_pkg ) {
1294 '', # 1 | N/A-Leave Empty CHAR 2
1295 '', # 2 | N/A-Leave Empty CHAR 15
1296 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1297 $self->invnum, # 4 | Invoice Number CHAR 15
1298 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1299 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1300 $item->{'amount'}, # 7 | Amount NUM* 9
1301 '', # 8 | Line Format Control** CHAR 2
1302 '', # 9 | Grouping Code CHAR 2
1303 '', # 10 | User Defined CHAR 15
1306 $detail .= $csv->string. "\n";
1312 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1314 my($pkg, $setup, $recur, $sdate, $edate);
1315 if ( $cust_bill_pkg->pkgnum ) {
1317 ($pkg, $setup, $recur, $sdate, $edate) = (
1318 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1319 ( $cust_bill_pkg->setup != 0
1320 ? sprintf("%.2f", $cust_bill_pkg->setup )
1322 ( $cust_bill_pkg->recur != 0
1323 ? sprintf("%.2f", $cust_bill_pkg->recur )
1325 ( $cust_bill_pkg->sdate
1326 ? time2str("%x", $cust_bill_pkg->sdate)
1328 ($cust_bill_pkg->edate
1329 ?time2str("%x", $cust_bill_pkg->edate)
1333 } else { #pkgnum tax
1334 next unless $cust_bill_pkg->setup != 0;
1335 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1336 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1338 ($pkg, $setup, $recur, $sdate, $edate) =
1339 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1345 ( map { '' } (1..11) ),
1346 ($pkg, $setup, $recur, $sdate, $edate)
1347 ) or die "can't create csv";
1349 $detail .= $csv->string. "\n";
1355 ( $header, $detail );
1361 Pays this invoice with a compliemntary payment. If there is an error,
1362 returns the error, otherwise returns false.
1368 my $cust_pay = new FS::cust_pay ( {
1369 'invnum' => $self->invnum,
1370 'paid' => $self->owed,
1373 'payinfo' => $self->cust_main->payinfo,
1381 Attempts to pay this invoice with a credit card payment via a
1382 Business::OnlinePayment realtime gateway. See
1383 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1384 for supported processors.
1390 $self->realtime_bop( 'CC', @_ );
1395 Attempts to pay this invoice with an electronic check (ACH) payment via a
1396 Business::OnlinePayment realtime gateway. See
1397 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1398 for supported processors.
1404 $self->realtime_bop( 'ECHECK', @_ );
1409 Attempts to pay this invoice with phone bill (LEC) payment via a
1410 Business::OnlinePayment realtime gateway. See
1411 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1412 for supported processors.
1418 $self->realtime_bop( 'LEC', @_ );
1422 my( $self, $method ) = @_;
1424 my $cust_main = $self->cust_main;
1425 my $balance = $cust_main->balance;
1426 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1427 $amount = sprintf("%.2f", $amount);
1428 return "not run (balance $balance)" unless $amount > 0;
1430 my $description = 'Internet Services';
1431 if ( $conf->exists('business-onlinepayment-description') ) {
1432 my $dtempl = $conf->config('business-onlinepayment-description');
1434 my $agent_obj = $cust_main->agent
1435 or die "can't retreive agent for $cust_main (agentnum ".
1436 $cust_main->agentnum. ")";
1437 my $agent = $agent_obj->agent;
1438 my $pkgs = join(', ',
1439 map { $_->cust_pkg->part_pkg->pkg }
1440 grep { $_->pkgnum } $self->cust_bill_pkg
1442 $description = eval qq("$dtempl");
1445 $cust_main->realtime_bop($method, $amount,
1446 'description' => $description,
1447 'invnum' => $self->invnum,
1452 =item batch_card OPTION => VALUE...
1454 Adds a payment for this invoice to the pending credit card batch (see
1455 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1456 runs the payment using a realtime gateway.
1461 my ($self, %options) = @_;
1462 my $cust_main = $self->cust_main;
1464 $options{invnum} = $self->invnum;
1466 $cust_main->batch_card(%options);
1469 sub _agent_template {
1471 $self->cust_main->agent_template;
1474 sub _agent_invoice_from {
1476 $self->cust_main->agent_invoice_from;
1479 =item print_text [ TIME [ , TEMPLATE ] ]
1481 Returns an text invoice, as a list of lines.
1483 TIME an optional value used to control the printing of overdue messages. The
1484 default is now. It isn't the date of the invoice; that's the `_date' field.
1485 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1486 L<Time::Local> and L<Date::Parse> for conversion functions.
1490 #still some false laziness w/_items stuff (and send_csv)
1493 my( $self, $today, $template ) = @_;
1496 # my $invnum = $self->invnum;
1497 my $cust_main = $self->cust_main;
1498 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1499 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1501 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1502 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1503 #my $balance_due = $self->owed + $pr_total - $cr_total;
1504 my $balance_due = $self->owed + $pr_total;
1507 #my($description,$amount);
1511 unless ($conf->exists('disable_previous_balance')) {
1512 foreach ( @pr_cust_bill ) {
1514 "Previous Balance, Invoice #". $_->invnum.
1515 " (". time2str("%x",$_->_date). ")",
1516 $money_char. sprintf("%10.2f",$_->owed)
1519 if (@pr_cust_bill) {
1520 push @buf,['','-----------'];
1521 push @buf,[ 'Total Previous Balance',
1522 $money_char. sprintf("%10.2f",$pr_total ) ];
1528 foreach my $cust_bill_pkg (
1529 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1530 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1533 my $desc = $cust_bill_pkg->desc;
1535 if ( $cust_bill_pkg->pkgnum > 0 ) {
1537 if ( $cust_bill_pkg->setup != 0 ) {
1538 my $description = $desc;
1539 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1540 push @buf, [ $description,
1541 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1543 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1544 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1547 if ( $cust_bill_pkg->recur != 0 ) {
1550 ( $conf->exists('disable_line_item_date_ranges')
1552 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1553 time2str("%x", $cust_bill_pkg->edate) . ")"
1555 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1558 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1559 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1560 $cust_bill_pkg->sdate );
1563 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1565 } else { #pkgnum tax or one-shot line item
1567 if ( $cust_bill_pkg->setup != 0 ) {
1569 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1571 if ( $cust_bill_pkg->recur != 0 ) {
1572 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1573 . time2str("%x", $cust_bill_pkg->edate). ")",
1574 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1582 push @buf,['','-----------'];
1583 push @buf,[ ( $conf->exists('disable_previous_balance')
1585 : 'Total New Charges'),
1586 $money_char. sprintf("%10.2f",$self->charged) ];
1589 unless ($conf->exists('disable_previous_balance')) {
1590 push @buf,['','-----------'];
1591 push @buf,['Total Charges',
1592 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1596 foreach ( $self->cust_credited ) {
1598 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1600 my $reason = substr($_->cust_credit->reason,0,32);
1601 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1602 $reason = " ($reason) " if $reason;
1604 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1606 $money_char. sprintf("%10.2f",$_->amount)
1609 #foreach ( @cr_cust_credit ) {
1611 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1612 # $money_char. sprintf("%10.2f",$_->credited)
1616 #get & print payments
1617 foreach ( $self->cust_bill_pay ) {
1619 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1622 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1623 $money_char. sprintf("%10.2f",$_->amount )
1628 my $balance_due_msg = $self->balance_due_msg;
1630 push @buf,['','-----------'];
1631 push @buf,[$balance_due_msg, $money_char.
1632 sprintf("%10.2f", $balance_due ) ];
1635 #create the template
1636 $template ||= $self->_agent_template;
1637 my $templatefile = 'invoice_template';
1638 $templatefile .= "_$template" if length($template);
1639 my @invoice_template = $conf->config($templatefile)
1640 or die "cannot load config file $templatefile";
1643 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1644 /invoice_lines\((\d*)\)/;
1645 $invoice_lines += $1 || scalar(@buf);
1648 die "no invoice_lines() functions in template?" unless $wasfunc;
1649 my $invoice_template = new Text::Template (
1651 SOURCE => [ map "$_\n", @invoice_template ],
1652 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1653 $invoice_template->compile()
1654 or die "can't compile template: $Text::Template::ERROR";
1656 #setup template variables
1657 package FS::cust_bill::_template; #!
1658 use vars qw( $custnum $invnum $date $agent @address $overdue
1659 $page $total_pages @buf );
1661 $custnum = $self->custnum;
1662 $invnum = $self->invnum;
1663 $date = $self->_date;
1664 $agent = $self->cust_main->agent->agent;
1667 if ( $FS::cust_bill::invoice_lines ) {
1669 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1671 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1676 #format address (variable for the template)
1678 @address = ( '', '', '', '', '', '' );
1679 package FS::cust_bill; #!
1680 $FS::cust_bill::_template::address[$l++] =
1681 $cust_main->payname.
1682 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1683 ? " (P.O. #". $cust_main->payinfo. ")"
1687 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1688 if $cust_main->company;
1689 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1690 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1691 if $cust_main->address2;
1692 $FS::cust_bill::_template::address[$l++] =
1693 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1695 my $countrydefault = $conf->config('countrydefault') || 'US';
1696 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1697 unless $cust_main->country eq $countrydefault;
1699 # #overdue? (variable for the template)
1700 # $FS::cust_bill::_template::overdue = (
1702 # && $today > $self->_date
1703 ## && $self->printed > 1
1704 # && $self->printed > 0
1707 #and subroutine for the template
1708 sub FS::cust_bill::_template::invoice_lines {
1709 my $lines = shift || scalar(@buf);
1711 scalar(@buf) ? shift @buf : [ '', '' ];
1717 $FS::cust_bill::_template::page = 1;
1721 push @collect, split("\n",
1722 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1724 $FS::cust_bill::_template::page++;
1727 map "$_\n", @collect;
1731 =item print_latex [ TIME [ , TEMPLATE ] ]
1733 Internal method - returns a filename of a filled-in LaTeX template for this
1734 invoice (Note: add ".tex" to get the actual filename).
1736 See print_ps and print_pdf for methods that return PostScript and PDF output.
1738 TIME an optional value used to control the printing of overdue messages. The
1739 default is now. It isn't the date of the invoice; that's the `_date' field.
1740 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1741 L<Time::Local> and L<Date::Parse> for conversion functions.
1745 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1748 my( $self, $today, $template ) = @_;
1750 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1753 my $cust_main = $self->cust_main;
1754 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1755 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1757 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1758 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1759 #my $balance_due = $self->owed + $pr_total - $cr_total;
1760 my $balance_due = $self->owed + $pr_total;
1762 #create the template
1763 $template ||= $self->_agent_template;
1764 my $templatefile = 'invoice_latex';
1765 my $suffix = length($template) ? "_$template" : '';
1766 $templatefile .= $suffix;
1767 my @invoice_template = map "$_\n", $conf->config($templatefile)
1768 or die "cannot load config file $templatefile";
1770 my($format, $text_template);
1771 if ( grep { /^%%Detail/ } @invoice_template ) {
1772 #change this to a die when the old code is removed
1773 warn "old-style invoice template $templatefile; ".
1774 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1777 $format = 'Text::Template';
1778 $text_template = new Text::Template(
1780 SOURCE => \@invoice_template,
1781 DELIMITERS => [ '[@--', '--@]' ],
1784 $text_template->compile()
1785 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1789 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1790 $returnaddress = join("\n",
1791 $conf->config_orbase('invoice_latexreturnaddress', $template)
1794 $returnaddress = '~';
1797 my %invoice_data = (
1798 'custnum' => $self->custnum,
1799 'invnum' => $self->invnum,
1800 'date' => time2str('%b %o, %Y', $self->_date),
1801 'today' => time2str('%b %o, %Y', $today),
1802 'agent' => _latex_escape($cust_main->agent->agent),
1803 'agent_custid' => _latex_escape($cust_main->agent_custid),
1804 'payname' => _latex_escape($cust_main->payname),
1805 'company' => _latex_escape($cust_main->company),
1806 'address1' => _latex_escape($cust_main->address1),
1807 'address2' => _latex_escape($cust_main->address2),
1808 'city' => _latex_escape($cust_main->city),
1809 'state' => _latex_escape($cust_main->state),
1811 'zip' => _latex_escape($cust_main->zip),
1812 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1813 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1814 'returnaddress' => $returnaddress,
1816 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1817 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1818 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1819 'balance' => $balance_due,
1820 'ship_enable' => $conf->exists('invoice-ship_address'),
1821 'unitprices' => $conf->exists('invoice-unitprice'),
1824 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1825 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1826 my $method = $prefix.$_;
1827 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1830 my $countrydefault = $conf->config('countrydefault') || 'US';
1831 if ( $cust_main->country eq $countrydefault ) {
1832 $invoice_data{'country'} = '';
1834 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1837 $invoice_data{'notes'} =
1839 # #do variable substitutions in notes
1840 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1841 $conf->config_orbase('invoice_latexnotes', $template)
1843 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1846 #do variable substitution in coupon
1847 foreach my $include (qw( coupon )) {
1849 my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1851 my $inc_tt = new Text::Template (
1853 SOURCE => [ map "$_\n", @inc_src ],
1854 DELIMITERS => [ '[@--', '--@]' ],
1855 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1857 unless ( $inc_tt->compile() ) {
1858 my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1859 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1863 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1865 $invoice_data{$include} =~ s/\n+$//
1868 $invoice_data{'footer'} =~ s/\n+$//;
1869 $invoice_data{'smallfooter'} =~ s/\n+$//;
1870 $invoice_data{'notes'} =~ s/\n+$//;
1872 $invoice_data{'po_line'} =
1873 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1874 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1878 if ( $format eq 'old' ) {
1881 my @total_item = ();
1882 while ( @invoice_template ) {
1883 my $line = shift @invoice_template;
1885 if ( $line =~ /^%%Detail\s*$/ ) {
1887 while ( ( my $line_item_line = shift @invoice_template )
1888 !~ /^%%EndDetail\s*$/ ) {
1889 push @line_item, $line_item_line;
1891 foreach my $line_item ( $self->_items ) {
1892 #foreach my $line_item ( $self->_items_pkg ) {
1893 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1894 $invoice_data{'description'} =
1895 _latex_escape($line_item->{'description'});
1896 if ( exists $line_item->{'ext_description'} ) {
1897 $invoice_data{'description'} .=
1898 "\\tabularnewline\n~~".
1899 join( "\\tabularnewline\n~~",
1900 map _latex_escape($_), @{$line_item->{'ext_description'}}
1903 $invoice_data{'amount'} = $line_item->{'amount'};
1904 $invoice_data{'unit_amount'} = $line_item->{'unit_amount'};
1905 $invoice_data{'quantity'} = $line_item->{'quantity'};
1906 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1908 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1911 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1913 while ( ( my $total_item_line = shift @invoice_template )
1914 !~ /^%%EndTotalDetails\s*$/ ) {
1915 push @total_item, $total_item_line;
1918 my @total_fill = ();
1921 foreach my $tax ( $self->_items_tax ) {
1922 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1923 $taxtotal += $tax->{'amount'};
1924 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1926 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1931 $invoice_data{'total_item'} = 'Sub-total';
1932 $invoice_data{'total_amount'} =
1933 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1934 unshift @total_fill,
1935 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1939 $invoice_data{'total_item'} = '\textbf{Total}';
1940 $invoice_data{'total_amount'} =
1941 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1943 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1946 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1949 foreach my $credit ( $self->_items_credits ) {
1950 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1952 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1954 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1959 foreach my $payment ( $self->_items_payments ) {
1960 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1962 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1964 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1968 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1969 $invoice_data{'total_amount'} =
1970 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1972 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1975 push @filled_in, @total_fill;
1978 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1979 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1980 push @filled_in, $line;
1991 } elsif ( $format eq 'Text::Template' ) {
1993 my @detail_items = ();
1994 my @total_items = ();
1996 $invoice_data{'detail_items'} = \@detail_items;
1997 $invoice_data{'total_items'} = \@total_items;
1999 my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2000 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2002 ext_description => [],
2004 $detail->{'ref'} = $line_item->{'pkgnum'};
2005 $detail->{'quantity'} = 1;
2006 $detail->{'description'} = _latex_escape($line_item->{'description'});
2007 if ( exists $line_item->{'ext_description'} ) {
2008 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2010 $detail->{'amount'} = $line_item->{'amount'};
2011 $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2012 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2014 push @detail_items, $detail;
2019 foreach my $tax ( $self->_items_tax ) {
2021 $total->{'total_item'} = _latex_escape($tax->{'description'});
2022 $taxtotal += $tax->{'amount'};
2023 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2024 push @total_items, $total;
2029 $total->{'total_item'} = 'Sub-total';
2030 $total->{'total_amount'} =
2031 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2032 unshift @total_items, $total;
2037 $total->{'total_item'} = '\textbf{Total}';
2038 $total->{'total_amount'} =
2041 $self->charged + ( $conf->exists('disable_previous_balance')
2047 push @total_items, $total;
2050 unless ($conf->exists('disable_previous_balance')) {
2051 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2054 foreach my $credit ( $self->_items_credits ) {
2056 $total->{'total_item'} = _latex_escape($credit->{'description'});
2058 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2059 push @total_items, $total;
2063 foreach my $payment ( $self->_items_payments ) {
2065 $total->{'total_item'} = _latex_escape($payment->{'description'});
2067 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2068 push @total_items, $total;
2073 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2074 $total->{'total_amount'} =
2075 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2076 push @total_items, $total;
2081 die "guru meditation #54";
2084 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2085 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2089 ) or die "can't open temp file: $!\n";
2090 if ( $format eq 'old' ) {
2091 print $fh join('', @filled_in );
2092 } elsif ( $format eq 'Text::Template' ) {
2093 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2095 die "guru meditation #32";
2099 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2104 =item print_ps [ TIME [ , TEMPLATE ] ]
2106 Returns an postscript invoice, as a scalar.
2108 TIME an optional value used to control the printing of overdue messages. The
2109 default is now. It isn't the date of the invoice; that's the `_date' field.
2110 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2111 L<Time::Local> and L<Date::Parse> for conversion functions.
2118 my $file = $self->print_latex(@_);
2119 my $ps = generate_ps($file);
2124 =item print_pdf [ TIME [ , TEMPLATE ] ]
2126 Returns an PDF invoice, as a scalar.
2128 TIME an optional value used to control the printing of overdue messages. The
2129 default is now. It isn't the date of the invoice; that's the `_date' field.
2130 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2131 L<Time::Local> and L<Date::Parse> for conversion functions.
2138 my $file = $self->print_latex(@_);
2139 my $pdf = generate_pdf($file);
2144 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2146 Returns an HTML invoice, as a scalar.
2148 TIME an optional value used to control the printing of overdue messages. The
2149 default is now. It isn't the date of the invoice; that's the `_date' field.
2150 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2151 L<Time::Local> and L<Date::Parse> for conversion functions.
2153 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2154 when emailing the invoice as part of a multipart/related MIME email.
2158 #some falze laziness w/print_text and print_latex (and send_csv)
2160 my( $self, $today, $template, $cid ) = @_;
2163 my $cust_main = $self->cust_main;
2164 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2165 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2167 $template ||= $self->_agent_template;
2168 my $templatefile = 'invoice_html';
2169 my $suffix = length($template) ? "_$template" : '';
2170 $templatefile .= $suffix;
2171 my @html_template = map "$_\n", $conf->config($templatefile)
2172 or die "cannot load config file $templatefile";
2174 my $html_template = new Text::Template(
2176 SOURCE => \@html_template,
2177 DELIMITERS => [ '<%=', '%>' ],
2180 $html_template->compile()
2181 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2183 my %invoice_data = (
2184 'custnum' => $self->custnum,
2185 'invnum' => $self->invnum,
2186 'date' => time2str('%b %o, %Y', $self->_date),
2187 'today' => time2str('%b %o, %Y', $today),
2188 'agent' => encode_entities($cust_main->agent->agent),
2189 'agent_custid' => encode_entities($cust_main->agent_custid),
2190 'payname' => encode_entities($cust_main->payname),
2191 'company' => encode_entities($cust_main->company),
2192 'address1' => encode_entities($cust_main->address1),
2193 'address2' => encode_entities($cust_main->address2),
2194 'city' => encode_entities($cust_main->city),
2195 'state' => encode_entities($cust_main->state),
2196 'zip' => encode_entities($cust_main->zip),
2197 'terms' => $conf->config('invoice_default_terms')
2198 || 'Payable upon receipt',
2200 'template' => $template,
2201 'ship_enable' => $conf->exists('invoice-ship_address'),
2202 'unitprices' => $conf->exists('invoice-unitprice'),
2203 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2206 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2207 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2208 my $method = $prefix.$_;
2209 $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2213 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2214 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2216 $invoice_data{'returnaddress'} =
2217 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2219 $invoice_data{'returnaddress'} =
2222 s/\\\\\*?\s*$/<BR>/;
2223 s/\\hyphenation\{[\w\s\-]+\}//;
2226 $conf->config_orbase( 'invoice_latexreturnaddress',
2232 my $countrydefault = $conf->config('countrydefault') || 'US';
2233 if ( $cust_main->country eq $countrydefault ) {
2234 $invoice_data{'country'} = '';
2236 $invoice_data{'country'} =
2237 encode_entities(code2country($cust_main->country));
2241 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2242 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2244 $invoice_data{'notes'} =
2245 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2247 $invoice_data{'notes'} =
2249 s/%%(.*)$/<!-- $1 -->/g;
2250 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2251 s/\\begin\{enumerate\}/<ol>/g;
2253 s/\\end\{enumerate\}/<\/ol>/g;
2254 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2261 $conf->config_orbase('invoice_latexnotes', $template)
2265 # #do variable substitutions in notes
2266 # $invoice_data{'notes'} =
2268 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2269 # $conf->config_orbase('invoice_latexnotes', $suffix)
2273 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2274 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2276 $invoice_data{'footer'} =
2277 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2279 $invoice_data{'footer'} =
2280 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2281 $conf->config_orbase('invoice_latexfooter', $template)
2285 $invoice_data{'po_line'} =
2286 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2287 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2290 my $money_char = $conf->config('money_char') || '$';
2292 my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2293 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2295 ext_description => [],
2297 $detail->{'ref'} = $line_item->{'pkgnum'};
2298 $detail->{'description'} = encode_entities($line_item->{'description'});
2299 if ( exists $line_item->{'ext_description'} ) {
2300 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2302 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2303 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2305 push @{$invoice_data{'detail_items'}}, $detail;
2310 foreach my $tax ( $self->_items_tax ) {
2312 $total->{'total_item'} = encode_entities($tax->{'description'});
2313 $taxtotal += $tax->{'amount'};
2314 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2315 push @{$invoice_data{'total_items'}}, $total;
2320 $total->{'total_item'} = 'Sub-total';
2321 $total->{'total_amount'} =
2322 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2323 unshift @{$invoice_data{'total_items'}}, $total;
2326 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2329 $total->{'total_item'} = '<b>Total</b>';
2330 $total->{'total_amount'} =
2333 $self->charged + ( $conf->exists('disable_previous_balance')
2339 push @{$invoice_data{'total_items'}}, $total;
2342 unless ($conf->exists('disable_previous_balance')) {
2343 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2346 foreach my $credit ( $self->_items_credits ) {
2348 $total->{'total_item'} = encode_entities($credit->{'description'});
2350 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2351 push @{$invoice_data{'total_items'}}, $total;
2355 foreach my $payment ( $self->_items_payments ) {
2357 $total->{'total_item'} = encode_entities($payment->{'description'});
2359 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2360 push @{$invoice_data{'total_items'}}, $total;
2365 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2366 $total->{'total_amount'} =
2367 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2368 push @{$invoice_data{'total_items'}}, $total;
2372 $html_template->fill_in( HASH => \%invoice_data);
2375 # quick subroutine for print_latex
2377 # There are ten characters that LaTeX treats as special characters, which
2378 # means that they do not simply typeset themselves:
2379 # # $ % & ~ _ ^ \ { }
2381 # TeX ignores blanks following an escaped character; if you want a blank (as
2382 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2386 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2387 $value =~ s/([<>])/\$$1\$/g;
2391 #utility methods for print_*
2393 sub balance_due_msg {
2395 my $msg = 'Balance Due';
2396 return $msg unless $conf->exists('invoice_default_terms');
2397 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2398 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2399 } elsif ( $conf->config('invoice_default_terms') ) {
2400 $msg .= ' - '. $conf->config('invoice_default_terms');
2405 =item invnum_date_pretty
2407 Returns a string with the invoice number and date, for example:
2408 "Invoice #54 (3/20/2008)"
2412 sub invnum_date_pretty {
2414 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2420 #my @display = scalar(@_)
2422 # : qw( _items_previous _items_pkg );
2423 # #: qw( _items_pkg );
2424 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2425 my @display = qw( _items_previous _items_pkg );
2428 foreach my $display ( @display ) {
2429 push @b, $self->$display(@_);
2434 sub _items_previous {
2436 my $cust_main = $self->cust_main;
2437 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2439 foreach ( @pr_cust_bill ) {
2441 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2442 ' ('. time2str('%x',$_->_date). ')',
2443 #'pkgpart' => 'N/A',
2445 'amount' => sprintf("%.2f", $_->owed),
2451 # 'description' => 'Previous Balance',
2452 # #'pkgpart' => 'N/A',
2453 # 'pkgnum' => 'N/A',
2454 # 'amount' => sprintf("%10.2f", $pr_total ),
2455 # 'ext_description' => [ map {
2456 # "Invoice ". $_->invnum.
2457 # " (". time2str("%x",$_->_date). ") ".
2458 # sprintf("%10.2f", $_->owed)
2459 # } @pr_cust_bill ],
2466 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2467 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2472 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2473 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2476 sub _items_cust_bill_pkg {
2478 my $cust_bill_pkg = shift;
2481 my $format = $opt{format} || '';
2482 my $escape_function = $opt{escape_function} || sub { shift };
2485 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2487 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2489 my $desc = $cust_bill_pkg->desc;
2491 my %details_opt = ( 'format' => $format,
2492 'escape_function' => $escape_function,
2495 if ( $cust_bill_pkg->pkgnum > 0 ) {
2497 if ( $cust_bill_pkg->setup != 0 ) {
2499 my $description = $desc;
2500 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2502 my @d = map &{$escape_function}($_),
2503 $cust_pkg->h_labels_short($self->_date);
2504 push @d, $cust_bill_pkg->details(%details_opt)
2505 if $cust_bill_pkg->recur == 0;
2508 description => $description,
2509 #pkgpart => $part_pkg->pkgpart,
2510 pkgnum => $cust_bill_pkg->pkgnum,
2511 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2512 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2513 quantity => $cust_bill_pkg->quantity,
2514 ext_description => \@d,
2518 if ( $cust_bill_pkg->recur != 0 ) {
2520 my $description = $desc;
2521 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2522 $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2523 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2526 #at least until cust_bill_pkg has "past" ranges in addition to
2527 #the "future" sdate/edate ones... see #3032
2528 my @d = map &{$escape_function}($_),
2529 $cust_pkg->h_labels_short($self->_date);
2530 #$cust_bill_pkg->edate,
2531 #$cust_bill_pkg->sdate),
2532 push @d, $cust_bill_pkg->details(%details_opt);
2535 description => $description,
2536 #pkgpart => $part_pkg->pkgpart,
2537 pkgnum => $cust_bill_pkg->pkgnum,
2538 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2539 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2540 quantity => $cust_bill_pkg->quantity,
2541 ext_description => \@d,
2546 } else { #pkgnum tax or one-shot line item (??)
2548 if ( $cust_bill_pkg->setup != 0 ) {
2550 'description' => $desc,
2551 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2554 if ( $cust_bill_pkg->recur != 0 ) {
2556 'description' => "$desc (".
2557 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2558 time2str("%x", $cust_bill_pkg->edate). ')',
2559 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2571 sub _items_credits {
2576 foreach ( $self->cust_credited ) {
2578 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2580 my $reason = $_->cust_credit->reason;
2581 #my $reason = substr($_->cust_credit->reason,0,32);
2582 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2583 $reason = " ($reason) " if $reason;
2585 #'description' => 'Credit ref\#'. $_->crednum.
2586 # " (". time2str("%x",$_->cust_credit->_date) .")".
2588 'description' => 'Credit applied '.
2589 time2str("%x",$_->cust_credit->_date). $reason,
2590 'amount' => sprintf("%.2f",$_->amount),
2593 #foreach ( @cr_cust_credit ) {
2595 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2596 # $money_char. sprintf("%10.2f",$_->credited)
2604 sub _items_payments {
2608 #get & print payments
2609 foreach ( $self->cust_bill_pay ) {
2611 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2614 'description' => "Payment received ".
2615 time2str("%x",$_->cust_pay->_date ),
2616 'amount' => sprintf("%.2f", $_->amount )
2635 sub process_reprint {
2636 process_re_X('print', @_);
2643 sub process_reemail {
2644 process_re_X('email', @_);
2652 process_re_X('fax', @_);
2655 use Storable qw(thaw);
2659 my( $method, $job ) = ( shift, shift );
2660 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2662 my $param = thaw(decode_base64(shift));
2663 warn Dumper($param) if $DEBUG;
2674 my($method, $job, %param ) = @_;
2676 warn "re_X $method for job $job with param:\n".
2677 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2680 #some false laziness w/search/cust_bill.html
2682 my $orderby = 'ORDER BY cust_bill._date';
2684 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2686 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2688 my @cust_bill = qsearch( {
2689 #'select' => "cust_bill.*",
2690 'table' => 'cust_bill',
2691 'addl_from' => $addl_from,
2693 'extra_sql' => $extra_sql,
2694 'order_by' => $orderby,
2698 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2701 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2702 foreach my $cust_bill ( @cust_bill ) {
2703 $cust_bill->$method();
2705 if ( $job ) { #progressbar foo
2707 if ( time - $min_sec > $last ) {
2708 my $error = $job->update_statustext(
2709 int( 100 * $num / scalar(@cust_bill) )
2711 die $error if $error;
2722 =head1 CLASS METHODS
2728 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2734 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2739 Returns an SQL fragment to retreive the net amount (charged minus credited).
2745 'charged - '. $class->credited_sql;
2750 Returns an SQL fragment to retreive the amount paid against this invoice.
2756 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2757 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2762 Returns an SQL fragment to retreive the amount credited against this invoice.
2768 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2769 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2772 =item search_sql HASHREF
2774 Class method which returns an SQL WHERE fragment to search for parameters
2775 specified in HASHREF. Valid parameters are
2781 Epoch date (UNIX timestamp) setting a lower bound for _date values
2785 Epoch date (UNIX timestamp) setting an upper bound for _date values
2799 =item newest_percust
2803 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2808 my($class, $param) = @_;
2810 warn "$me search_sql called with params: \n".
2811 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2816 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2817 push @search, "cust_bill._date >= $1";
2819 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2820 push @search, "cust_bill._date < $1";
2822 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2823 push @search, "cust_bill.invnum >= $1";
2825 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2826 push @search, "cust_bill.invnum <= $1";
2828 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2829 push @search, "cust_main.agentnum = $1";
2832 push @search, '0 != '. FS::cust_bill->owed_sql
2833 if $param->{'open'};
2835 push @search, '0 != '. FS::cust_bill->net_sql
2838 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2839 if $param->{'days'};
2841 if ( $param->{'newest_percust'} ) {
2843 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2844 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2846 my @newest_where = map { my $x = $_;
2847 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2850 grep ! /^cust_main./, @search;
2851 my $newest_where = scalar(@newest_where)
2852 ? ' AND '. join(' AND ', @newest_where)
2856 push @search, "cust_bill._date = (
2857 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2858 WHERE newest_cust_bill.custnum = cust_bill.custnum
2864 my $curuser = $FS::CurrentUser::CurrentUser;
2865 if ( $curuser->username eq 'fs_queue'
2866 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2868 my $newuser = qsearchs('access_user', {
2869 'username' => $username,
2873 $curuser = $newuser;
2875 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2879 push @search, $curuser->agentnums_sql;
2881 join(' AND ', @search );
2893 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2894 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base