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 ftp_invoice [ TEMPLATENAME ]
918 Sends this invoice data via FTP.
920 TEMPLATENAME is unused?
926 my $template = scalar(@_) ? shift : '';
930 'server' => $conf->config('cust_bill-ftpserver'),
931 'username' => $conf->config('cust_bill-ftpusername'),
932 'password' => $conf->config('cust_bill-ftppassword'),
933 'dir' => $conf->config('cust_bill-ftpdir'),
934 'format' => $conf->config('cust_bill-ftpformat'),
938 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
940 Like B<send>, but only sends the invoice if it is the newest open invoice for
950 grep { $_->owed > 0 }
951 qsearch('cust_bill', {
952 'custnum' => $self->custnum,
953 #'_date' => { op=>'>', value=>$self->_date },
954 'invnum' => { op=>'>', value=>$self->invnum },
961 =item send_csv OPTION => VALUE, ...
963 Sends invoice as a CSV data-file to a remote host with the specified protocol.
967 protocol - currently only "ftp"
973 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
974 and YYMMDDHHMMSS is a timestamp.
976 See L</print_csv> for a description of the output format.
981 my($self, %opt) = @_;
985 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
986 mkdir $spooldir, 0700 unless -d $spooldir;
988 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
989 my $file = "$spooldir/$tracctnum.csv";
991 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
993 open(CSV, ">$file") or die "can't open $file: $!";
1001 if ( $opt{protocol} eq 'ftp' ) {
1002 eval "use Net::FTP;";
1004 $net = Net::FTP->new($opt{server}) or die @$;
1006 die "unknown protocol: $opt{protocol}";
1009 $net->login( $opt{username}, $opt{password} )
1010 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1012 $net->binary or die "can't set binary mode";
1014 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1016 $net->put($file) or die "can't put $file: $!";
1026 Spools CSV invoice data.
1032 =item format - 'default' or 'billco'
1034 =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>).
1036 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1038 =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.
1045 my($self, %opt) = @_;
1047 my $cust_main = $self->cust_main;
1049 if ( $opt{'dest'} ) {
1050 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1051 $cust_main->invoicing_list;
1052 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1053 || ! keys %invoicing_list;
1056 if ( $opt{'balanceover'} ) {
1058 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1061 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1062 mkdir $spooldir, 0700 unless -d $spooldir;
1064 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1068 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1069 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1072 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1074 open(CSV, ">>$file") or die "can't open $file: $!";
1075 flock(CSV, LOCK_EX);
1080 if ( lc($opt{'format'}) eq 'billco' ) {
1082 flock(CSV, LOCK_UN);
1087 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1090 open(CSV,">>$file") or die "can't open $file: $!";
1091 flock(CSV, LOCK_EX);
1097 flock(CSV, LOCK_UN);
1104 =item print_csv OPTION => VALUE, ...
1106 Returns CSV data for this invoice.
1110 format - 'default' or 'billco'
1112 Returns a list consisting of two scalars. The first is a single line of CSV
1113 header information for this invoice. The second is one or more lines of CSV
1114 detail information for this invoice.
1116 If I<format> is not specified or "default", the fields of the CSV file are as
1119 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1123 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1125 B<record_type> is C<cust_bill> for the initial header line only. The
1126 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1127 fields are filled in.
1129 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1130 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1133 =item invnum - invoice number
1135 =item custnum - customer number
1137 =item _date - invoice date
1139 =item charged - total invoice amount
1141 =item first - customer first name
1143 =item last - customer first name
1145 =item company - company name
1147 =item address1 - address line 1
1149 =item address2 - address line 1
1159 =item pkg - line item description
1161 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1163 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1165 =item sdate - start date for recurring fee
1167 =item edate - end date for recurring fee
1171 If I<format> is "billco", the fields of the header CSV file are as follows:
1173 +-------------------------------------------------------------------+
1174 | FORMAT HEADER FILE |
1175 |-------------------------------------------------------------------|
1176 | Field | Description | Name | Type | Width |
1177 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1178 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1179 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1180 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1181 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1182 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1183 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1184 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1185 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1186 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1187 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1188 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1189 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1190 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1191 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1192 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1193 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1194 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1195 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1196 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1197 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1198 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1199 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1200 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1201 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1202 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1203 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1204 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1205 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1206 +-------+-------------------------------+------------+------+-------+
1208 If I<format> is "billco", the fields of the detail CSV file are as follows:
1210 FORMAT FOR DETAIL FILE
1212 Field | Description | Name | Type | Width
1213 1 | N/A-Leave Empty | RC | CHAR | 2
1214 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1215 3 | Account Number | TRACCTNUM | CHAR | 15
1216 4 | Invoice Number | TRINVOICE | CHAR | 15
1217 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1218 6 | Transaction Detail | DETAILS | CHAR | 100
1219 7 | Amount | AMT | NUM* | 9
1220 8 | Line Format Control** | LNCTRL | CHAR | 2
1221 9 | Grouping Code | GROUP | CHAR | 2
1222 10 | User Defined | ACCT CODE | CHAR | 15
1227 my($self, %opt) = @_;
1229 eval "use Text::CSV_XS";
1232 my $cust_main = $self->cust_main;
1234 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1236 if ( lc($opt{'format'}) eq 'billco' ) {
1239 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1241 my $duedate = $self->balance_due_date;
1243 my( $previous_balance, @unused ) = $self->previous; #previous balance
1245 my $pmt_cr_applied = 0;
1246 $pmt_cr_applied += $_->{'amount'}
1247 foreach ( $self->_items_payments, $self->_items_credits ) ;
1249 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1252 '', # 1 | N/A-Leave Empty CHAR 2
1253 '', # 2 | N/A-Leave Empty CHAR 15
1254 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1255 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1256 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1257 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1258 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1259 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1260 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1261 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1262 '', # 10 | Ancillary Billing Information CHAR 30
1263 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1264 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1267 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1270 $duedate, # 14 | Bill Due Date CHAR 10
1272 $previous_balance, # 15 | Previous Balance NUM* 9
1273 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1274 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1275 $totaldue, # 18 | Total Amt Due NUM* 9
1276 $totaldue, # 19 | Total Amt Due NUM* 9
1277 '', # 20 | 30 Day Aging NUM* 9
1278 '', # 21 | 60 Day Aging NUM* 9
1279 '', # 22 | 90 Day Aging NUM* 9
1280 'N', # 23 | Y/N CHAR 1
1281 '', # 24 | Remittance automation CHAR 100
1282 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1283 $self->custnum, # 26 | Customer Reference Number CHAR 15
1284 '0', # 27 | Federal Tax*** NUM* 9
1285 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1286 '0', # 29 | Other Taxes & Fees*** NUM* 9
1295 time2str("%x", $self->_date),
1296 sprintf("%.2f", $self->charged),
1297 ( map { $cust_main->getfield($_) }
1298 qw( first last company address1 address2 city state zip country ) ),
1300 ) or die "can't create csv";
1303 my $header = $csv->string. "\n";
1306 if ( lc($opt{'format'}) eq 'billco' ) {
1309 foreach my $item ( $self->_items_pkg ) {
1312 '', # 1 | N/A-Leave Empty CHAR 2
1313 '', # 2 | N/A-Leave Empty CHAR 15
1314 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1315 $self->invnum, # 4 | Invoice Number CHAR 15
1316 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1317 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1318 $item->{'amount'}, # 7 | Amount NUM* 9
1319 '', # 8 | Line Format Control** CHAR 2
1320 '', # 9 | Grouping Code CHAR 2
1321 '', # 10 | User Defined CHAR 15
1324 $detail .= $csv->string. "\n";
1330 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1332 my($pkg, $setup, $recur, $sdate, $edate);
1333 if ( $cust_bill_pkg->pkgnum ) {
1335 ($pkg, $setup, $recur, $sdate, $edate) = (
1336 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1337 ( $cust_bill_pkg->setup != 0
1338 ? sprintf("%.2f", $cust_bill_pkg->setup )
1340 ( $cust_bill_pkg->recur != 0
1341 ? sprintf("%.2f", $cust_bill_pkg->recur )
1343 ( $cust_bill_pkg->sdate
1344 ? time2str("%x", $cust_bill_pkg->sdate)
1346 ($cust_bill_pkg->edate
1347 ?time2str("%x", $cust_bill_pkg->edate)
1351 } else { #pkgnum tax
1352 next unless $cust_bill_pkg->setup != 0;
1353 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1354 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1356 ($pkg, $setup, $recur, $sdate, $edate) =
1357 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1363 ( map { '' } (1..11) ),
1364 ($pkg, $setup, $recur, $sdate, $edate)
1365 ) or die "can't create csv";
1367 $detail .= $csv->string. "\n";
1373 ( $header, $detail );
1379 Pays this invoice with a compliemntary payment. If there is an error,
1380 returns the error, otherwise returns false.
1386 my $cust_pay = new FS::cust_pay ( {
1387 'invnum' => $self->invnum,
1388 'paid' => $self->owed,
1391 'payinfo' => $self->cust_main->payinfo,
1399 Attempts to pay this invoice with a credit card payment via a
1400 Business::OnlinePayment realtime gateway. See
1401 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1402 for supported processors.
1408 $self->realtime_bop( 'CC', @_ );
1413 Attempts to pay this invoice with an electronic check (ACH) payment via a
1414 Business::OnlinePayment realtime gateway. See
1415 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1416 for supported processors.
1422 $self->realtime_bop( 'ECHECK', @_ );
1427 Attempts to pay this invoice with phone bill (LEC) payment via a
1428 Business::OnlinePayment realtime gateway. See
1429 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1430 for supported processors.
1436 $self->realtime_bop( 'LEC', @_ );
1440 my( $self, $method ) = @_;
1442 my $cust_main = $self->cust_main;
1443 my $balance = $cust_main->balance;
1444 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1445 $amount = sprintf("%.2f", $amount);
1446 return "not run (balance $balance)" unless $amount > 0;
1448 my $description = 'Internet Services';
1449 if ( $conf->exists('business-onlinepayment-description') ) {
1450 my $dtempl = $conf->config('business-onlinepayment-description');
1452 my $agent_obj = $cust_main->agent
1453 or die "can't retreive agent for $cust_main (agentnum ".
1454 $cust_main->agentnum. ")";
1455 my $agent = $agent_obj->agent;
1456 my $pkgs = join(', ',
1457 map { $_->cust_pkg->part_pkg->pkg }
1458 grep { $_->pkgnum } $self->cust_bill_pkg
1460 $description = eval qq("$dtempl");
1463 $cust_main->realtime_bop($method, $amount,
1464 'description' => $description,
1465 'invnum' => $self->invnum,
1470 =item batch_card OPTION => VALUE...
1472 Adds a payment for this invoice to the pending credit card batch (see
1473 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1474 runs the payment using a realtime gateway.
1479 my ($self, %options) = @_;
1480 my $cust_main = $self->cust_main;
1482 $options{invnum} = $self->invnum;
1484 $cust_main->batch_card(%options);
1487 sub _agent_template {
1489 $self->cust_main->agent_template;
1492 sub _agent_invoice_from {
1494 $self->cust_main->agent_invoice_from;
1497 =item print_text [ TIME [ , TEMPLATE ] ]
1499 Returns an text invoice, as a list of lines.
1501 TIME an optional value used to control the printing of overdue messages. The
1502 default is now. It isn't the date of the invoice; that's the `_date' field.
1503 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1504 L<Time::Local> and L<Date::Parse> for conversion functions.
1508 #still some false laziness w/_items stuff (and send_csv)
1511 my( $self, $today, $template ) = @_;
1514 # my $invnum = $self->invnum;
1515 my $cust_main = $self->cust_main;
1516 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1517 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1519 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1520 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1521 #my $balance_due = $self->owed + $pr_total - $cr_total;
1522 my $balance_due = $self->owed + $pr_total;
1525 #my($description,$amount);
1529 unless ($conf->exists('disable_previous_balance')) {
1530 foreach ( @pr_cust_bill ) {
1532 "Previous Balance, Invoice #". $_->invnum.
1533 " (". time2str("%x",$_->_date). ")",
1534 $money_char. sprintf("%10.2f",$_->owed)
1537 if (@pr_cust_bill) {
1538 push @buf,['','-----------'];
1539 push @buf,[ 'Total Previous Balance',
1540 $money_char. sprintf("%10.2f",$pr_total ) ];
1546 foreach my $cust_bill_pkg (
1547 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1548 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1551 my $desc = $cust_bill_pkg->desc;
1553 if ( $cust_bill_pkg->pkgnum > 0 ) {
1555 if ( $cust_bill_pkg->setup != 0 ) {
1556 my $description = $desc;
1557 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1558 push @buf, [ $description,
1559 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1561 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1562 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1565 if ( $cust_bill_pkg->recur != 0 ) {
1568 ( $conf->exists('disable_line_item_date_ranges')
1570 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1571 time2str("%x", $cust_bill_pkg->edate) . ")"
1573 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1576 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1577 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1578 $cust_bill_pkg->sdate );
1581 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1583 } else { #pkgnum tax or one-shot line item
1585 if ( $cust_bill_pkg->setup != 0 ) {
1587 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1589 if ( $cust_bill_pkg->recur != 0 ) {
1590 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1591 . time2str("%x", $cust_bill_pkg->edate). ")",
1592 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1600 push @buf,['','-----------'];
1601 push @buf,[ ( $conf->exists('disable_previous_balance')
1603 : 'Total New Charges'),
1604 $money_char. sprintf("%10.2f",$self->charged) ];
1607 unless ($conf->exists('disable_previous_balance')) {
1608 push @buf,['','-----------'];
1609 push @buf,['Total Charges',
1610 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1614 foreach ( $self->cust_credited ) {
1616 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1618 my $reason = substr($_->cust_credit->reason,0,32);
1619 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1620 $reason = " ($reason) " if $reason;
1622 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1624 $money_char. sprintf("%10.2f",$_->amount)
1627 #foreach ( @cr_cust_credit ) {
1629 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1630 # $money_char. sprintf("%10.2f",$_->credited)
1634 #get & print payments
1635 foreach ( $self->cust_bill_pay ) {
1637 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1640 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1641 $money_char. sprintf("%10.2f",$_->amount )
1646 my $balance_due_msg = $self->balance_due_msg;
1648 push @buf,['','-----------'];
1649 push @buf,[$balance_due_msg, $money_char.
1650 sprintf("%10.2f", $balance_due ) ];
1653 #create the template
1654 $template ||= $self->_agent_template;
1655 my $templatefile = 'invoice_template';
1656 $templatefile .= "_$template" if length($template);
1657 my @invoice_template = $conf->config($templatefile)
1658 or die "cannot load config file $templatefile";
1661 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1662 /invoice_lines\((\d*)\)/;
1663 $invoice_lines += $1 || scalar(@buf);
1666 die "no invoice_lines() functions in template?" unless $wasfunc;
1667 my $invoice_template = new Text::Template (
1669 SOURCE => [ map "$_\n", @invoice_template ],
1670 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1671 $invoice_template->compile()
1672 or die "can't compile template: $Text::Template::ERROR";
1674 #setup template variables
1675 package FS::cust_bill::_template; #!
1676 use vars qw( $custnum $invnum $date $agent @address $overdue
1677 $page $total_pages @buf );
1679 $custnum = $self->custnum;
1680 $invnum = $self->invnum;
1681 $date = $self->_date;
1682 $agent = $self->cust_main->agent->agent;
1685 if ( $FS::cust_bill::invoice_lines ) {
1687 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1689 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1694 #format address (variable for the template)
1696 @address = ( '', '', '', '', '', '' );
1697 package FS::cust_bill; #!
1698 $FS::cust_bill::_template::address[$l++] =
1699 $cust_main->payname.
1700 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1701 ? " (P.O. #". $cust_main->payinfo. ")"
1705 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1706 if $cust_main->company;
1707 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1708 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1709 if $cust_main->address2;
1710 $FS::cust_bill::_template::address[$l++] =
1711 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1713 my $countrydefault = $conf->config('countrydefault') || 'US';
1714 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1715 unless $cust_main->country eq $countrydefault;
1717 # #overdue? (variable for the template)
1718 # $FS::cust_bill::_template::overdue = (
1720 # && $today > $self->_date
1721 ## && $self->printed > 1
1722 # && $self->printed > 0
1725 #and subroutine for the template
1726 sub FS::cust_bill::_template::invoice_lines {
1727 my $lines = shift || scalar(@buf);
1729 scalar(@buf) ? shift @buf : [ '', '' ];
1735 $FS::cust_bill::_template::page = 1;
1739 push @collect, split("\n",
1740 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1742 $FS::cust_bill::_template::page++;
1745 map "$_\n", @collect;
1749 =item print_latex [ TIME [ , TEMPLATE ] ]
1751 Internal method - returns a filename of a filled-in LaTeX template for this
1752 invoice (Note: add ".tex" to get the actual filename).
1754 See print_ps and print_pdf for methods that return PostScript and PDF output.
1756 TIME an optional value used to control the printing of overdue messages. The
1757 default is now. It isn't the date of the invoice; that's the `_date' field.
1758 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1759 L<Time::Local> and L<Date::Parse> for conversion functions.
1763 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1766 my( $self, $today, $template ) = @_;
1768 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1771 my $cust_main = $self->cust_main;
1772 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1773 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1775 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1776 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1777 #my $balance_due = $self->owed + $pr_total - $cr_total;
1778 my $balance_due = $self->owed + $pr_total;
1780 #create the template
1781 $template ||= $self->_agent_template;
1782 my $templatefile = 'invoice_latex';
1783 my $suffix = length($template) ? "_$template" : '';
1784 $templatefile .= $suffix;
1785 my @invoice_template = map "$_\n", $conf->config($templatefile)
1786 or die "cannot load config file $templatefile";
1788 my($format, $text_template);
1789 if ( grep { /^%%Detail/ } @invoice_template ) {
1790 #change this to a die when the old code is removed
1791 warn "old-style invoice template $templatefile; ".
1792 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1795 $format = 'Text::Template';
1796 $text_template = new Text::Template(
1798 SOURCE => \@invoice_template,
1799 DELIMITERS => [ '[@--', '--@]' ],
1802 $text_template->compile()
1803 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1807 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1808 $returnaddress = join("\n",
1809 $conf->config_orbase('invoice_latexreturnaddress', $template)
1812 $returnaddress = '~';
1815 my %invoice_data = (
1816 'custnum' => $self->custnum,
1817 'invnum' => $self->invnum,
1818 'date' => time2str('%b %o, %Y', $self->_date),
1819 'today' => time2str('%b %o, %Y', $today),
1820 'agent' => _latex_escape($cust_main->agent->agent),
1821 'agent_custid' => _latex_escape($cust_main->agent_custid),
1822 'payname' => _latex_escape($cust_main->payname),
1823 'company' => _latex_escape($cust_main->company),
1824 'address1' => _latex_escape($cust_main->address1),
1825 'address2' => _latex_escape($cust_main->address2),
1826 'city' => _latex_escape($cust_main->city),
1827 'state' => _latex_escape($cust_main->state),
1829 'zip' => _latex_escape($cust_main->zip),
1830 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1831 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1832 'returnaddress' => $returnaddress,
1834 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1835 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1836 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1837 'current_charges' => sprintf('%.2f', $self->charged ),
1838 'previous_balance' => sprintf("%.2f", $pr_total),
1839 'balance' => sprintf("%.2f", $balance_due),
1840 'duedate' => $self->balance_due_date,
1841 'ship_enable' => $conf->exists('invoice-ship_address'),
1842 'unitprices' => $conf->exists('invoice-unitprice'),
1845 my $countrydefault = $conf->config('countrydefault') || 'US';
1846 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1847 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1848 my $method = $prefix.$_;
1849 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1851 $invoice_data{'ship_country'} = ''
1852 if ( $invoice_data{'ship_country'} eq $countrydefault );
1854 if ( $cust_main->country eq $countrydefault ) {
1855 $invoice_data{'country'} = '';
1857 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1860 $invoice_data{'notes'} =
1862 # #do variable substitutions in notes
1863 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1864 $conf->config_orbase('invoice_latexnotes', $template)
1866 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1869 #do variable substitution in coupon
1870 foreach my $include (qw( coupon )) {
1872 my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1874 my $inc_tt = new Text::Template (
1876 SOURCE => [ map "$_\n", @inc_src ],
1877 DELIMITERS => [ '[@--', '--@]' ],
1878 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1880 unless ( $inc_tt->compile() ) {
1881 my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1882 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1886 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1888 $invoice_data{$include} =~ s/\n+$//
1891 $invoice_data{'footer'} =~ s/\n+$//;
1892 $invoice_data{'smallfooter'} =~ s/\n+$//;
1893 $invoice_data{'notes'} =~ s/\n+$//;
1895 $invoice_data{'po_line'} =
1896 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1897 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1901 if ( $format eq 'old' ) {
1904 my @total_item = ();
1905 while ( @invoice_template ) {
1906 my $line = shift @invoice_template;
1908 if ( $line =~ /^%%Detail\s*$/ ) {
1910 while ( ( my $line_item_line = shift @invoice_template )
1911 !~ /^%%EndDetail\s*$/ ) {
1912 push @line_item, $line_item_line;
1914 foreach my $line_item ( $self->_items ) {
1915 #foreach my $line_item ( $self->_items_pkg ) {
1916 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1917 $invoice_data{'description'} =
1918 _latex_escape($line_item->{'description'});
1919 if ( exists $line_item->{'ext_description'} ) {
1920 $invoice_data{'description'} .=
1921 "\\tabularnewline\n~~".
1922 join( "\\tabularnewline\n~~",
1923 map _latex_escape($_), @{$line_item->{'ext_description'}}
1926 $invoice_data{'amount'} = $line_item->{'amount'};
1927 $invoice_data{'unit_amount'} = $line_item->{'unit_amount'};
1928 $invoice_data{'quantity'} = $line_item->{'quantity'};
1929 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1931 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1934 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1936 while ( ( my $total_item_line = shift @invoice_template )
1937 !~ /^%%EndTotalDetails\s*$/ ) {
1938 push @total_item, $total_item_line;
1941 my @total_fill = ();
1944 foreach my $tax ( $self->_items_tax ) {
1945 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1946 $taxtotal += $tax->{'amount'};
1947 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1949 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1954 $invoice_data{'total_item'} = 'Sub-total';
1955 $invoice_data{'total_amount'} =
1956 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1957 unshift @total_fill,
1958 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1962 $invoice_data{'total_item'} = '\textbf{Total}';
1963 $invoice_data{'total_amount'} =
1964 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1966 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1969 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1972 foreach my $credit ( $self->_items_credits ) {
1973 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1975 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1977 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1982 foreach my $payment ( $self->_items_payments ) {
1983 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1985 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1987 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1991 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1992 $invoice_data{'total_amount'} =
1993 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1995 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1998 push @filled_in, @total_fill;
2001 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2002 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2003 push @filled_in, $line;
2014 } elsif ( $format eq 'Text::Template' ) {
2016 my @detail_items = ();
2017 my @total_items = ();
2019 $invoice_data{'detail_items'} = \@detail_items;
2020 $invoice_data{'total_items'} = \@total_items;
2022 my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2023 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2025 ext_description => [],
2027 $detail->{'ref'} = $line_item->{'pkgnum'};
2028 $detail->{'quantity'} = 1;
2029 $detail->{'description'} = _latex_escape($line_item->{'description'});
2030 if ( exists $line_item->{'ext_description'} ) {
2031 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2033 $detail->{'amount'} = $line_item->{'amount'};
2034 $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2035 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2037 push @detail_items, $detail;
2042 foreach my $tax ( $self->_items_tax ) {
2044 $total->{'total_item'} = _latex_escape($tax->{'description'});
2045 $taxtotal += $tax->{'amount'};
2046 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2047 push @total_items, $total;
2051 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2053 $total->{'total_item'} = 'Sub-total';
2054 $total->{'total_amount'} =
2055 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2056 unshift @total_items, $total;
2058 $invoice_data{'taxtotal'} = '0.00';
2063 $total->{'total_item'} = '\textbf{Total}';
2064 $total->{'total_amount'} =
2067 $self->charged + ( $conf->exists('disable_previous_balance')
2073 push @total_items, $total;
2076 unless ($conf->exists('disable_previous_balance')) {
2077 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2080 my $credittotal = 0;
2081 foreach my $credit ( $self->_items_credits ) {
2083 $total->{'total_item'} = _latex_escape($credit->{'description'});
2084 $credittotal += $credit->{'amount'};
2085 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2086 push @total_items, $total;
2088 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2091 my $paymenttotal = 0;
2092 foreach my $payment ( $self->_items_payments ) {
2094 $total->{'total_item'} = _latex_escape($payment->{'description'});
2095 $paymenttotal += $payment->{'amount'};
2096 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2097 push @total_items, $total;
2099 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2103 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2104 $total->{'total_amount'} =
2105 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2106 push @total_items, $total;
2111 die "guru meditation #54";
2114 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2115 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2119 ) or die "can't open temp file: $!\n";
2120 if ( $format eq 'old' ) {
2121 print $fh join('', @filled_in );
2122 } elsif ( $format eq 'Text::Template' ) {
2123 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2125 die "guru meditation #32";
2129 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2134 =item print_ps [ TIME [ , TEMPLATE ] ]
2136 Returns an postscript invoice, as a scalar.
2138 TIME an optional value used to control the printing of overdue messages. The
2139 default is now. It isn't the date of the invoice; that's the `_date' field.
2140 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2141 L<Time::Local> and L<Date::Parse> for conversion functions.
2148 my $file = $self->print_latex(@_);
2149 my $ps = generate_ps($file);
2154 =item print_pdf [ TIME [ , TEMPLATE ] ]
2156 Returns an PDF invoice, as a scalar.
2158 TIME an optional value used to control the printing of overdue messages. The
2159 default is now. It isn't the date of the invoice; that's the `_date' field.
2160 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2161 L<Time::Local> and L<Date::Parse> for conversion functions.
2168 my $file = $self->print_latex(@_);
2169 my $pdf = generate_pdf($file);
2174 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2176 Returns an HTML invoice, as a scalar.
2178 TIME an optional value used to control the printing of overdue messages. The
2179 default is now. It isn't the date of the invoice; that's the `_date' field.
2180 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2181 L<Time::Local> and L<Date::Parse> for conversion functions.
2183 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2184 when emailing the invoice as part of a multipart/related MIME email.
2188 #some falze laziness w/print_text and print_latex (and send_csv)
2190 my( $self, $today, $template, $cid ) = @_;
2193 my $cust_main = $self->cust_main;
2194 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2195 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2197 $template ||= $self->_agent_template;
2198 my $templatefile = 'invoice_html';
2199 my $suffix = length($template) ? "_$template" : '';
2200 $templatefile .= $suffix;
2201 my @html_template = map "$_\n", $conf->config($templatefile)
2202 or die "cannot load config file $templatefile";
2204 my $html_template = new Text::Template(
2206 SOURCE => \@html_template,
2207 DELIMITERS => [ '<%=', '%>' ],
2210 $html_template->compile()
2211 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2213 my %invoice_data = (
2214 'custnum' => $self->custnum,
2215 'invnum' => $self->invnum,
2216 'date' => time2str('%b %o, %Y', $self->_date),
2217 'today' => time2str('%b %o, %Y', $today),
2218 'agent' => encode_entities($cust_main->agent->agent),
2219 'agent_custid' => encode_entities($cust_main->agent_custid),
2220 'payname' => encode_entities($cust_main->payname),
2221 'company' => encode_entities($cust_main->company),
2222 'address1' => encode_entities($cust_main->address1),
2223 'address2' => encode_entities($cust_main->address2),
2224 'city' => encode_entities($cust_main->city),
2225 'state' => encode_entities($cust_main->state),
2226 'zip' => encode_entities($cust_main->zip),
2227 'terms' => $conf->config('invoice_default_terms')
2228 || 'Payable upon receipt',
2230 'template' => $template,
2231 'ship_enable' => $conf->exists('invoice-ship_address'),
2232 'unitprices' => $conf->exists('invoice-unitprice'),
2233 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2236 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2237 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2238 my $method = $prefix.$_;
2239 $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2243 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2244 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2246 $invoice_data{'returnaddress'} =
2247 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2249 $invoice_data{'returnaddress'} =
2252 s/\\\\\*?\s*$/<BR>/;
2253 s/\\hyphenation\{[\w\s\-]+\}//;
2256 $conf->config_orbase( 'invoice_latexreturnaddress',
2262 my $countrydefault = $conf->config('countrydefault') || 'US';
2263 if ( $cust_main->country eq $countrydefault ) {
2264 $invoice_data{'country'} = '';
2266 $invoice_data{'country'} =
2267 encode_entities(code2country($cust_main->country));
2271 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2272 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2274 $invoice_data{'notes'} =
2275 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2277 $invoice_data{'notes'} =
2279 s/%%(.*)$/<!-- $1 -->/g;
2280 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2281 s/\\begin\{enumerate\}/<ol>/g;
2283 s/\\end\{enumerate\}/<\/ol>/g;
2284 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2291 $conf->config_orbase('invoice_latexnotes', $template)
2295 # #do variable substitutions in notes
2296 # $invoice_data{'notes'} =
2298 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2299 # $conf->config_orbase('invoice_latexnotes', $suffix)
2303 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2304 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2306 $invoice_data{'footer'} =
2307 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2309 $invoice_data{'footer'} =
2310 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2311 $conf->config_orbase('invoice_latexfooter', $template)
2315 $invoice_data{'po_line'} =
2316 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2317 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2320 my $money_char = $conf->config('money_char') || '$';
2322 my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2323 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2325 ext_description => [],
2327 $detail->{'ref'} = $line_item->{'pkgnum'};
2328 $detail->{'description'} = encode_entities($line_item->{'description'});
2329 if ( exists $line_item->{'ext_description'} ) {
2330 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2332 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2333 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2335 push @{$invoice_data{'detail_items'}}, $detail;
2340 foreach my $tax ( $self->_items_tax ) {
2342 $total->{'total_item'} = encode_entities($tax->{'description'});
2343 $taxtotal += $tax->{'amount'};
2344 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2345 push @{$invoice_data{'total_items'}}, $total;
2350 $total->{'total_item'} = 'Sub-total';
2351 $total->{'total_amount'} =
2352 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2353 unshift @{$invoice_data{'total_items'}}, $total;
2356 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2359 $total->{'total_item'} = '<b>Total</b>';
2360 $total->{'total_amount'} =
2363 $self->charged + ( $conf->exists('disable_previous_balance')
2369 push @{$invoice_data{'total_items'}}, $total;
2372 unless ($conf->exists('disable_previous_balance')) {
2373 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2376 foreach my $credit ( $self->_items_credits ) {
2378 $total->{'total_item'} = encode_entities($credit->{'description'});
2380 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2381 push @{$invoice_data{'total_items'}}, $total;
2385 foreach my $payment ( $self->_items_payments ) {
2387 $total->{'total_item'} = encode_entities($payment->{'description'});
2389 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2390 push @{$invoice_data{'total_items'}}, $total;
2395 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2396 $total->{'total_amount'} =
2397 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2398 push @{$invoice_data{'total_items'}}, $total;
2402 $html_template->fill_in( HASH => \%invoice_data);
2405 # quick subroutine for print_latex
2407 # There are ten characters that LaTeX treats as special characters, which
2408 # means that they do not simply typeset themselves:
2409 # # $ % & ~ _ ^ \ { }
2411 # TeX ignores blanks following an escaped character; if you want a blank (as
2412 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2416 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2417 $value =~ s/([<>])/\$$1\$/g;
2421 #utility methods for print_*
2423 sub balance_due_msg {
2425 my $msg = 'Balance Due';
2426 return $msg unless $conf->exists('invoice_default_terms');
2427 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2428 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2429 } elsif ( $conf->config('invoice_default_terms') ) {
2430 $msg .= ' - '. $conf->config('invoice_default_terms');
2435 sub balance_due_date {
2438 if ( $conf->exists('invoice_default_terms')
2439 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2440 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2445 =item invnum_date_pretty
2447 Returns a string with the invoice number and date, for example:
2448 "Invoice #54 (3/20/2008)"
2452 sub invnum_date_pretty {
2454 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2460 #my @display = scalar(@_)
2462 # : qw( _items_previous _items_pkg );
2463 # #: qw( _items_pkg );
2464 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2465 my @display = qw( _items_previous _items_pkg );
2468 foreach my $display ( @display ) {
2469 push @b, $self->$display(@_);
2474 sub _items_previous {
2476 my $cust_main = $self->cust_main;
2477 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2479 foreach ( @pr_cust_bill ) {
2481 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2482 ' ('. time2str('%x',$_->_date). ')',
2483 #'pkgpart' => 'N/A',
2485 'amount' => sprintf("%.2f", $_->owed),
2491 # 'description' => 'Previous Balance',
2492 # #'pkgpart' => 'N/A',
2493 # 'pkgnum' => 'N/A',
2494 # 'amount' => sprintf("%10.2f", $pr_total ),
2495 # 'ext_description' => [ map {
2496 # "Invoice ". $_->invnum.
2497 # " (". time2str("%x",$_->_date). ") ".
2498 # sprintf("%10.2f", $_->owed)
2499 # } @pr_cust_bill ],
2506 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2507 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2512 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2513 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2516 sub _items_cust_bill_pkg {
2518 my $cust_bill_pkg = shift;
2521 my $format = $opt{format} || '';
2522 my $escape_function = $opt{escape_function} || sub { shift };
2525 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2527 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2529 my $desc = $cust_bill_pkg->desc;
2531 my %details_opt = ( 'format' => $format,
2532 'escape_function' => $escape_function,
2535 if ( $cust_bill_pkg->pkgnum > 0 ) {
2537 if ( $cust_bill_pkg->setup != 0 ) {
2539 my $description = $desc;
2540 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2542 my @d = map &{$escape_function}($_),
2543 $cust_pkg->h_labels_short($self->_date);
2544 push @d, $cust_bill_pkg->details(%details_opt)
2545 if $cust_bill_pkg->recur == 0;
2548 description => $description,
2549 #pkgpart => $part_pkg->pkgpart,
2550 pkgnum => $cust_bill_pkg->pkgnum,
2551 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2552 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2553 quantity => $cust_bill_pkg->quantity,
2554 ext_description => \@d,
2558 if ( $cust_bill_pkg->recur != 0 ) {
2560 my $description = $desc;
2561 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2562 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2563 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2566 #at least until cust_bill_pkg has "past" ranges in addition to
2567 #the "future" sdate/edate ones... see #3032
2568 my @d = map &{$escape_function}($_),
2569 $cust_pkg->h_labels_short($self->_date);
2570 #$cust_bill_pkg->edate,
2571 #$cust_bill_pkg->sdate),
2572 push @d, $cust_bill_pkg->details(%details_opt);
2575 description => $description,
2576 #pkgpart => $part_pkg->pkgpart,
2577 pkgnum => $cust_bill_pkg->pkgnum,
2578 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2579 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2580 quantity => $cust_bill_pkg->quantity,
2581 ext_description => \@d,
2586 } else { #pkgnum tax or one-shot line item (??)
2588 if ( $cust_bill_pkg->setup != 0 ) {
2590 'description' => $desc,
2591 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2594 if ( $cust_bill_pkg->recur != 0 ) {
2596 'description' => "$desc (".
2597 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2598 time2str("%x", $cust_bill_pkg->edate). ')',
2599 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2611 sub _items_credits {
2616 foreach ( $self->cust_credited ) {
2618 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2620 my $reason = $_->cust_credit->reason;
2621 #my $reason = substr($_->cust_credit->reason,0,32);
2622 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2623 $reason = " ($reason) " if $reason;
2625 #'description' => 'Credit ref\#'. $_->crednum.
2626 # " (". time2str("%x",$_->cust_credit->_date) .")".
2628 'description' => 'Credit applied '.
2629 time2str("%x",$_->cust_credit->_date). $reason,
2630 'amount' => sprintf("%.2f",$_->amount),
2633 #foreach ( @cr_cust_credit ) {
2635 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2636 # $money_char. sprintf("%10.2f",$_->credited)
2644 sub _items_payments {
2648 #get & print payments
2649 foreach ( $self->cust_bill_pay ) {
2651 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2654 'description' => "Payment received ".
2655 time2str("%x",$_->cust_pay->_date ),
2656 'amount' => sprintf("%.2f", $_->amount )
2675 sub process_reprint {
2676 process_re_X('print', @_);
2683 sub process_reemail {
2684 process_re_X('email', @_);
2692 process_re_X('fax', @_);
2700 process_re_X('ftp', @_);
2703 use Storable qw(thaw);
2707 my( $method, $job ) = ( shift, shift );
2708 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2710 my $param = thaw(decode_base64(shift));
2711 warn Dumper($param) if $DEBUG;
2722 my($method, $job, %param ) = @_;
2724 warn "re_X $method for job $job with param:\n".
2725 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2728 #some false laziness w/search/cust_bill.html
2730 my $orderby = 'ORDER BY cust_bill._date';
2732 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2734 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2736 my @cust_bill = qsearch( {
2737 #'select' => "cust_bill.*",
2738 'table' => 'cust_bill',
2739 'addl_from' => $addl_from,
2741 'extra_sql' => $extra_sql,
2742 'order_by' => $orderby,
2746 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2748 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2751 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2752 foreach my $cust_bill ( @cust_bill ) {
2753 $cust_bill->$method();
2755 if ( $job ) { #progressbar foo
2757 if ( time - $min_sec > $last ) {
2758 my $error = $job->update_statustext(
2759 int( 100 * $num / scalar(@cust_bill) )
2761 die $error if $error;
2772 =head1 CLASS METHODS
2778 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2784 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2789 Returns an SQL fragment to retreive the net amount (charged minus credited).
2795 'charged - '. $class->credited_sql;
2800 Returns an SQL fragment to retreive the amount paid against this invoice.
2806 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2807 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2812 Returns an SQL fragment to retreive the amount credited against this invoice.
2818 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2819 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2822 =item search_sql HASHREF
2824 Class method which returns an SQL WHERE fragment to search for parameters
2825 specified in HASHREF. Valid parameters are
2831 Epoch date (UNIX timestamp) setting a lower bound for _date values
2835 Epoch date (UNIX timestamp) setting an upper bound for _date values
2849 =item newest_percust
2853 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2858 my($class, $param) = @_;
2860 warn "$me search_sql called with params: \n".
2861 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2866 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2867 push @search, "cust_bill._date >= $1";
2869 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2870 push @search, "cust_bill._date < $1";
2872 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2873 push @search, "cust_bill.invnum >= $1";
2875 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2876 push @search, "cust_bill.invnum <= $1";
2878 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2879 push @search, "cust_main.agentnum = $1";
2882 push @search, '0 != '. FS::cust_bill->owed_sql
2883 if $param->{'open'};
2885 push @search, '0 != '. FS::cust_bill->net_sql
2888 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2889 if $param->{'days'};
2891 if ( $param->{'newest_percust'} ) {
2893 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2894 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2896 my @newest_where = map { my $x = $_;
2897 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2900 grep ! /^cust_main./, @search;
2901 my $newest_where = scalar(@newest_where)
2902 ? ' AND '. join(' AND ', @newest_where)
2906 push @search, "cust_bill._date = (
2907 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2908 WHERE newest_cust_bill.custnum = cust_bill.custnum
2914 my $curuser = $FS::CurrentUser::CurrentUser;
2915 if ( $curuser->username eq 'fs_queue'
2916 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2918 my $newuser = qsearchs('access_user', {
2919 'username' => $username,
2923 $curuser = $newuser;
2925 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2929 push @search, $curuser->agentnums_sql;
2931 join(' AND ', @search );
2943 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2944 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base