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 $subject = $self->email_subject($template);
850 my $error = send_email(
851 $self->generate_email(
852 'from' => $invoice_from,
853 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
854 'subject' => $subject,
855 'template' => $template,
858 die "can't email invoice: $error\n" if $error;
859 #die "$error\n" if $error;
866 #my $template = scalar(@_) ? shift : '';
869 my $subject = $conf->config('invoice_subject') || 'Invoice';
871 my $cust_main = $self->cust_main;
872 my $name = $cust_main->name;
873 my $name_short = $cust_main->name_short;
874 my $invoice_number = $self->invnum;
875 my $invoice_date = $self->_date_pretty;
880 =item lpr_data [ TEMPLATENAME ]
882 Returns the postscript or plaintext for this invoice as an arrayref.
884 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
889 my( $self, $template) = @_;
890 $conf->exists('invoice_latex')
891 ? [ $self->print_ps('', $template) ]
892 : [ $self->print_text('', $template) ];
895 =item print [ TEMPLATENAME ]
899 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
906 my $template = scalar(@_) ? shift : '';
908 do_print $self->lpr_data($template);
911 =item fax_invoice [ TEMPLATENAME ]
915 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
921 my $template = scalar(@_) ? shift : '';
923 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
924 unless $conf->exists('invoice_latex');
926 my $dialstring = $self->cust_main->getfield('fax');
929 my $error = send_fax( 'docdata' => $self->lpr_data($template),
930 'dialstring' => $dialstring,
932 die $error if $error;
936 =item ftp_invoice [ TEMPLATENAME ]
938 Sends this invoice data via FTP.
940 TEMPLATENAME is unused?
946 my $template = scalar(@_) ? shift : '';
950 'server' => $conf->config('cust_bill-ftpserver'),
951 'username' => $conf->config('cust_bill-ftpusername'),
952 'password' => $conf->config('cust_bill-ftppassword'),
953 'dir' => $conf->config('cust_bill-ftpdir'),
954 'format' => $conf->config('cust_bill-ftpformat'),
958 =item spool_invoice [ TEMPLATENAME ]
960 Spools this invoice data (see L<FS::spool_csv>)
962 TEMPLATENAME is unused?
968 my $template = scalar(@_) ? shift : '';
971 'format' => $conf->config('cust_bill-spoolformat'),
972 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
976 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
978 Like B<send>, but only sends the invoice if it is the newest open invoice for
988 grep { $_->owed > 0 }
989 qsearch('cust_bill', {
990 'custnum' => $self->custnum,
991 #'_date' => { op=>'>', value=>$self->_date },
992 'invnum' => { op=>'>', value=>$self->invnum },
999 =item send_csv OPTION => VALUE, ...
1001 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1005 protocol - currently only "ftp"
1011 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1012 and YYMMDDHHMMSS is a timestamp.
1014 See L</print_csv> for a description of the output format.
1019 my($self, %opt) = @_;
1023 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1024 mkdir $spooldir, 0700 unless -d $spooldir;
1026 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1027 my $file = "$spooldir/$tracctnum.csv";
1029 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1031 open(CSV, ">$file") or die "can't open $file: $!";
1039 if ( $opt{protocol} eq 'ftp' ) {
1040 eval "use Net::FTP;";
1042 $net = Net::FTP->new($opt{server}) or die @$;
1044 die "unknown protocol: $opt{protocol}";
1047 $net->login( $opt{username}, $opt{password} )
1048 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1050 $net->binary or die "can't set binary mode";
1052 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1054 $net->put($file) or die "can't put $file: $!";
1064 Spools CSV invoice data.
1070 =item format - 'default' or 'billco'
1072 =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>).
1074 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1076 =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.
1083 my($self, %opt) = @_;
1085 my $cust_main = $self->cust_main;
1087 if ( $opt{'dest'} ) {
1088 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1089 $cust_main->invoicing_list;
1090 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1091 || ! keys %invoicing_list;
1094 if ( $opt{'balanceover'} ) {
1096 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1099 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1100 mkdir $spooldir, 0700 unless -d $spooldir;
1102 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1106 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1107 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1110 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1112 open(CSV, ">>$file") or die "can't open $file: $!";
1113 flock(CSV, LOCK_EX);
1118 if ( lc($opt{'format'}) eq 'billco' ) {
1120 flock(CSV, LOCK_UN);
1125 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1128 open(CSV,">>$file") or die "can't open $file: $!";
1129 flock(CSV, LOCK_EX);
1135 flock(CSV, LOCK_UN);
1142 =item print_csv OPTION => VALUE, ...
1144 Returns CSV data for this invoice.
1148 format - 'default' or 'billco'
1150 Returns a list consisting of two scalars. The first is a single line of CSV
1151 header information for this invoice. The second is one or more lines of CSV
1152 detail information for this invoice.
1154 If I<format> is not specified or "default", the fields of the CSV file are as
1157 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1161 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1163 B<record_type> is C<cust_bill> for the initial header line only. The
1164 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1165 fields are filled in.
1167 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1168 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1171 =item invnum - invoice number
1173 =item custnum - customer number
1175 =item _date - invoice date
1177 =item charged - total invoice amount
1179 =item first - customer first name
1181 =item last - customer first name
1183 =item company - company name
1185 =item address1 - address line 1
1187 =item address2 - address line 1
1197 =item pkg - line item description
1199 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1201 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1203 =item sdate - start date for recurring fee
1205 =item edate - end date for recurring fee
1209 If I<format> is "billco", the fields of the header CSV file are as follows:
1211 +-------------------------------------------------------------------+
1212 | FORMAT HEADER FILE |
1213 |-------------------------------------------------------------------|
1214 | Field | Description | Name | Type | Width |
1215 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1216 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1217 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1218 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1219 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1220 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1221 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1222 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1223 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1224 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1225 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1226 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1227 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1228 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1229 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1230 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1231 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1232 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1233 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1234 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1235 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1236 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1237 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1238 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1239 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1240 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1241 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1242 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1243 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1244 +-------+-------------------------------+------------+------+-------+
1246 If I<format> is "billco", the fields of the detail CSV file are as follows:
1248 FORMAT FOR DETAIL FILE
1250 Field | Description | Name | Type | Width
1251 1 | N/A-Leave Empty | RC | CHAR | 2
1252 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1253 3 | Account Number | TRACCTNUM | CHAR | 15
1254 4 | Invoice Number | TRINVOICE | CHAR | 15
1255 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1256 6 | Transaction Detail | DETAILS | CHAR | 100
1257 7 | Amount | AMT | NUM* | 9
1258 8 | Line Format Control** | LNCTRL | CHAR | 2
1259 9 | Grouping Code | GROUP | CHAR | 2
1260 10 | User Defined | ACCT CODE | CHAR | 15
1265 my($self, %opt) = @_;
1267 eval "use Text::CSV_XS";
1270 my $cust_main = $self->cust_main;
1272 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1274 if ( lc($opt{'format'}) eq 'billco' ) {
1277 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1279 my $duedate = $self->balance_due_date;
1281 my( $previous_balance, @unused ) = $self->previous; #previous balance
1283 my $pmt_cr_applied = 0;
1284 $pmt_cr_applied += $_->{'amount'}
1285 foreach ( $self->_items_payments, $self->_items_credits ) ;
1287 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1290 '', # 1 | N/A-Leave Empty CHAR 2
1291 '', # 2 | N/A-Leave Empty CHAR 15
1292 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1293 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1294 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1295 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1296 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1297 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1298 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1299 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1300 '', # 10 | Ancillary Billing Information CHAR 30
1301 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1302 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1305 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1308 $duedate, # 14 | Bill Due Date CHAR 10
1310 $previous_balance, # 15 | Previous Balance NUM* 9
1311 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1312 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1313 $totaldue, # 18 | Total Amt Due NUM* 9
1314 $totaldue, # 19 | Total Amt Due NUM* 9
1315 '', # 20 | 30 Day Aging NUM* 9
1316 '', # 21 | 60 Day Aging NUM* 9
1317 '', # 22 | 90 Day Aging NUM* 9
1318 'N', # 23 | Y/N CHAR 1
1319 '', # 24 | Remittance automation CHAR 100
1320 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1321 $self->custnum, # 26 | Customer Reference Number CHAR 15
1322 '0', # 27 | Federal Tax*** NUM* 9
1323 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1324 '0', # 29 | Other Taxes & Fees*** NUM* 9
1333 time2str("%x", $self->_date),
1334 sprintf("%.2f", $self->charged),
1335 ( map { $cust_main->getfield($_) }
1336 qw( first last company address1 address2 city state zip country ) ),
1338 ) or die "can't create csv";
1341 my $header = $csv->string. "\n";
1344 if ( lc($opt{'format'}) eq 'billco' ) {
1347 foreach my $item ( $self->_items_pkg ) {
1350 '', # 1 | N/A-Leave Empty CHAR 2
1351 '', # 2 | N/A-Leave Empty CHAR 15
1352 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1353 $self->invnum, # 4 | Invoice Number CHAR 15
1354 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1355 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1356 $item->{'amount'}, # 7 | Amount NUM* 9
1357 '', # 8 | Line Format Control** CHAR 2
1358 '', # 9 | Grouping Code CHAR 2
1359 '', # 10 | User Defined CHAR 15
1362 $detail .= $csv->string. "\n";
1368 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1370 my($pkg, $setup, $recur, $sdate, $edate);
1371 if ( $cust_bill_pkg->pkgnum ) {
1373 ($pkg, $setup, $recur, $sdate, $edate) = (
1374 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1375 ( $cust_bill_pkg->setup != 0
1376 ? sprintf("%.2f", $cust_bill_pkg->setup )
1378 ( $cust_bill_pkg->recur != 0
1379 ? sprintf("%.2f", $cust_bill_pkg->recur )
1381 ( $cust_bill_pkg->sdate
1382 ? time2str("%x", $cust_bill_pkg->sdate)
1384 ($cust_bill_pkg->edate
1385 ?time2str("%x", $cust_bill_pkg->edate)
1389 } else { #pkgnum tax
1390 next unless $cust_bill_pkg->setup != 0;
1391 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1392 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1394 ($pkg, $setup, $recur, $sdate, $edate) =
1395 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1401 ( map { '' } (1..11) ),
1402 ($pkg, $setup, $recur, $sdate, $edate)
1403 ) or die "can't create csv";
1405 $detail .= $csv->string. "\n";
1411 ( $header, $detail );
1417 Pays this invoice with a compliemntary payment. If there is an error,
1418 returns the error, otherwise returns false.
1424 my $cust_pay = new FS::cust_pay ( {
1425 'invnum' => $self->invnum,
1426 'paid' => $self->owed,
1429 'payinfo' => $self->cust_main->payinfo,
1437 Attempts to pay this invoice with a credit card payment via a
1438 Business::OnlinePayment realtime gateway. See
1439 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1440 for supported processors.
1446 $self->realtime_bop( 'CC', @_ );
1451 Attempts to pay this invoice with an electronic check (ACH) payment via a
1452 Business::OnlinePayment realtime gateway. See
1453 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1454 for supported processors.
1460 $self->realtime_bop( 'ECHECK', @_ );
1465 Attempts to pay this invoice with phone bill (LEC) payment via a
1466 Business::OnlinePayment realtime gateway. See
1467 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1468 for supported processors.
1474 $self->realtime_bop( 'LEC', @_ );
1478 my( $self, $method ) = @_;
1480 my $cust_main = $self->cust_main;
1481 my $balance = $cust_main->balance;
1482 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1483 $amount = sprintf("%.2f", $amount);
1484 return "not run (balance $balance)" unless $amount > 0;
1486 my $description = 'Internet Services';
1487 if ( $conf->exists('business-onlinepayment-description') ) {
1488 my $dtempl = $conf->config('business-onlinepayment-description');
1490 my $agent_obj = $cust_main->agent
1491 or die "can't retreive agent for $cust_main (agentnum ".
1492 $cust_main->agentnum. ")";
1493 my $agent = $agent_obj->agent;
1494 my $pkgs = join(', ',
1495 map { $_->cust_pkg->part_pkg->pkg }
1496 grep { $_->pkgnum } $self->cust_bill_pkg
1498 $description = eval qq("$dtempl");
1501 $cust_main->realtime_bop($method, $amount,
1502 'description' => $description,
1503 'invnum' => $self->invnum,
1508 =item batch_card OPTION => VALUE...
1510 Adds a payment for this invoice to the pending credit card batch (see
1511 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1512 runs the payment using a realtime gateway.
1517 my ($self, %options) = @_;
1518 my $cust_main = $self->cust_main;
1520 $options{invnum} = $self->invnum;
1522 $cust_main->batch_card(%options);
1525 sub _agent_template {
1527 $self->cust_main->agent_template;
1530 sub _agent_invoice_from {
1532 $self->cust_main->agent_invoice_from;
1535 =item print_text [ TIME [ , TEMPLATE ] ]
1537 Returns an text invoice, as a list of lines.
1539 TIME an optional value used to control the printing of overdue messages. The
1540 default is now. It isn't the date of the invoice; that's the `_date' field.
1541 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1542 L<Time::Local> and L<Date::Parse> for conversion functions.
1546 #still some false laziness w/_items stuff (and send_csv)
1549 my( $self, $today, $template ) = @_;
1552 # my $invnum = $self->invnum;
1553 my $cust_main = $self->cust_main;
1554 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1555 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1557 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1558 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1559 #my $balance_due = $self->owed + $pr_total - $cr_total;
1560 my $balance_due = $self->owed + $pr_total;
1563 #my($description,$amount);
1567 unless ($conf->exists('disable_previous_balance')) {
1568 foreach ( @pr_cust_bill ) {
1570 "Previous Balance, Invoice #". $_->invnum.
1571 " (". time2str("%x",$_->_date). ")",
1572 $money_char. sprintf("%10.2f",$_->owed)
1575 if (@pr_cust_bill) {
1576 push @buf,['','-----------'];
1577 push @buf,[ 'Total Previous Balance',
1578 $money_char. sprintf("%10.2f",$pr_total ) ];
1584 foreach my $cust_bill_pkg (
1585 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1586 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1589 my $desc = $cust_bill_pkg->desc;
1591 if ( $cust_bill_pkg->pkgnum > 0 ) {
1593 if ( $cust_bill_pkg->setup != 0 ) {
1594 my $description = $desc;
1595 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1596 push @buf, [ $description,
1597 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1599 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1600 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1603 if ( $cust_bill_pkg->recur != 0 ) {
1606 ( $conf->exists('disable_line_item_date_ranges')
1608 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1609 time2str("%x", $cust_bill_pkg->edate) . ")"
1611 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1614 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1615 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1616 $cust_bill_pkg->sdate );
1619 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1621 } else { #pkgnum tax or one-shot line item
1623 if ( $cust_bill_pkg->setup != 0 ) {
1625 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1627 if ( $cust_bill_pkg->recur != 0 ) {
1628 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1629 . time2str("%x", $cust_bill_pkg->edate). ")",
1630 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1638 push @buf,['','-----------'];
1639 push @buf,[ ( $conf->exists('disable_previous_balance')
1641 : 'Total New Charges'),
1642 $money_char. sprintf("%10.2f",$self->charged) ];
1645 unless ($conf->exists('disable_previous_balance')) {
1646 push @buf,['','-----------'];
1647 push @buf,['Total Charges',
1648 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1652 foreach ( $self->cust_credited ) {
1654 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1656 my $reason = substr($_->cust_credit->reason,0,32);
1657 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1658 $reason = " ($reason) " if $reason;
1660 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1662 $money_char. sprintf("%10.2f",$_->amount)
1665 #foreach ( @cr_cust_credit ) {
1667 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1668 # $money_char. sprintf("%10.2f",$_->credited)
1672 #get & print payments
1673 foreach ( $self->cust_bill_pay ) {
1675 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1678 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1679 $money_char. sprintf("%10.2f",$_->amount )
1684 my $balance_due_msg = $self->balance_due_msg;
1686 push @buf,['','-----------'];
1687 push @buf,[$balance_due_msg, $money_char.
1688 sprintf("%10.2f", $balance_due ) ];
1691 #create the template
1692 $template ||= $self->_agent_template;
1693 my $templatefile = 'invoice_template';
1694 $templatefile .= "_$template" if length($template);
1695 my @invoice_template = $conf->config($templatefile)
1696 or die "cannot load config file $templatefile";
1699 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1700 /invoice_lines\((\d*)\)/;
1701 $invoice_lines += $1 || scalar(@buf);
1704 die "no invoice_lines() functions in template?" unless $wasfunc;
1705 my $invoice_template = new Text::Template (
1707 SOURCE => [ map "$_\n", @invoice_template ],
1708 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1709 $invoice_template->compile()
1710 or die "can't compile template: $Text::Template::ERROR";
1712 #setup template variables
1713 package FS::cust_bill::_template; #!
1714 use vars qw( $custnum $invnum $date $agent @address $overdue
1715 $page $total_pages @buf );
1717 $custnum = $self->custnum;
1718 $invnum = $self->invnum;
1719 $date = $self->_date;
1720 $agent = $self->cust_main->agent->agent;
1723 if ( $FS::cust_bill::invoice_lines ) {
1725 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1727 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1732 #format address (variable for the template)
1734 @address = ( '', '', '', '', '', '' );
1735 package FS::cust_bill; #!
1736 $FS::cust_bill::_template::address[$l++] =
1737 $cust_main->payname.
1738 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1739 ? " (P.O. #". $cust_main->payinfo. ")"
1743 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1744 if $cust_main->company;
1745 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1746 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1747 if $cust_main->address2;
1748 $FS::cust_bill::_template::address[$l++] =
1749 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1751 my $countrydefault = $conf->config('countrydefault') || 'US';
1752 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1753 unless $cust_main->country eq $countrydefault;
1755 # #overdue? (variable for the template)
1756 # $FS::cust_bill::_template::overdue = (
1758 # && $today > $self->_date
1759 ## && $self->printed > 1
1760 # && $self->printed > 0
1763 #and subroutine for the template
1764 sub FS::cust_bill::_template::invoice_lines {
1765 my $lines = shift || scalar(@buf);
1767 scalar(@buf) ? shift @buf : [ '', '' ];
1773 $FS::cust_bill::_template::page = 1;
1777 push @collect, split("\n",
1778 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1780 $FS::cust_bill::_template::page++;
1783 map "$_\n", @collect;
1787 =item print_latex [ TIME [ , TEMPLATE ] ]
1789 Internal method - returns a filename of a filled-in LaTeX template for this
1790 invoice (Note: add ".tex" to get the actual filename).
1792 See print_ps and print_pdf for methods that return PostScript and PDF output.
1794 TIME an optional value used to control the printing of overdue messages. The
1795 default is now. It isn't the date of the invoice; that's the `_date' field.
1796 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1797 L<Time::Local> and L<Date::Parse> for conversion functions.
1801 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1804 my( $self, $today, $template ) = @_;
1806 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1809 my $cust_main = $self->cust_main;
1810 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1811 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1813 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1814 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1815 #my $balance_due = $self->owed + $pr_total - $cr_total;
1816 my $balance_due = $self->owed + $pr_total;
1818 #create the template
1819 $template ||= $self->_agent_template;
1820 my $templatefile = 'invoice_latex';
1821 my $suffix = length($template) ? "_$template" : '';
1822 $templatefile .= $suffix;
1823 my @invoice_template = map "$_\n", $conf->config($templatefile)
1824 or die "cannot load config file $templatefile";
1826 my($format, $text_template);
1827 if ( grep { /^%%Detail/ } @invoice_template ) {
1828 #change this to a die when the old code is removed
1829 warn "old-style invoice template $templatefile; ".
1830 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1833 $format = 'Text::Template';
1834 $text_template = new Text::Template(
1836 SOURCE => \@invoice_template,
1837 DELIMITERS => [ '[@--', '--@]' ],
1840 $text_template->compile()
1841 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1845 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1846 $returnaddress = join("\n",
1847 $conf->config_orbase('invoice_latexreturnaddress', $template)
1850 $returnaddress = '~';
1853 my %invoice_data = (
1854 'custnum' => $self->custnum,
1855 'invnum' => $self->invnum,
1856 'date' => time2str('%b %o, %Y', $self->_date),
1857 'today' => time2str('%b %o, %Y', $today),
1858 'agent' => _latex_escape($cust_main->agent->agent),
1859 'agent_custid' => _latex_escape($cust_main->agent_custid),
1860 'payname' => _latex_escape($cust_main->payname),
1861 'company' => _latex_escape($cust_main->company),
1862 'address1' => _latex_escape($cust_main->address1),
1863 'address2' => _latex_escape($cust_main->address2),
1864 'city' => _latex_escape($cust_main->city),
1865 'state' => _latex_escape($cust_main->state),
1867 'zip' => _latex_escape($cust_main->zip),
1868 'fax' => _latex_escape($cust_main->fax),
1869 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1870 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1871 'returnaddress' => $returnaddress,
1873 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1874 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1875 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1876 'current_charges' => sprintf('%.2f', $self->charged ),
1877 'previous_balance' => sprintf("%.2f", $pr_total),
1878 'balance' => sprintf("%.2f", $balance_due),
1879 'duedate' => $self->balance_due_date,
1880 'ship_enable' => $conf->exists('invoice-ship_address'),
1881 'unitprices' => $conf->exists('invoice-unitprice'),
1884 my $countrydefault = $conf->config('countrydefault') || 'US';
1885 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1886 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1887 my $method = $prefix.$_;
1888 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1890 $invoice_data{'ship_country'} = ''
1891 if ( $invoice_data{'ship_country'} eq $countrydefault );
1893 if ( $cust_main->country eq $countrydefault ) {
1894 $invoice_data{'country'} = '';
1896 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1899 $invoice_data{'notes'} =
1901 # #do variable substitutions in notes
1902 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1903 $conf->config_orbase('invoice_latexnotes', $template)
1905 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1908 #do variable substitution in coupon
1909 foreach my $include (qw( coupon )) {
1911 my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1913 my $inc_tt = new Text::Template (
1915 SOURCE => [ map "$_\n", @inc_src ],
1916 DELIMITERS => [ '[@--', '--@]' ],
1917 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1919 unless ( $inc_tt->compile() ) {
1920 my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1921 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1925 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1927 $invoice_data{$include} =~ s/\n+$//
1930 $invoice_data{'footer'} =~ s/\n+$//;
1931 $invoice_data{'smallfooter'} =~ s/\n+$//;
1932 $invoice_data{'notes'} =~ s/\n+$//;
1934 $invoice_data{'po_line'} =
1935 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1936 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1940 if ( $format eq 'old' ) {
1943 my @total_item = ();
1944 while ( @invoice_template ) {
1945 my $line = shift @invoice_template;
1947 if ( $line =~ /^%%Detail\s*$/ ) {
1949 while ( ( my $line_item_line = shift @invoice_template )
1950 !~ /^%%EndDetail\s*$/ ) {
1951 push @line_item, $line_item_line;
1953 foreach my $line_item ( $self->_items ) { #( 'format'=>'latex' ) ) {
1954 #foreach my $line_item ( $self->_items_pkg ) {
1955 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1956 $invoice_data{'description'} =
1957 _latex_escape($line_item->{'description'});
1958 if ( exists $line_item->{'ext_description'} ) {
1959 $invoice_data{'description'} .=
1960 "\\tabularnewline\n~~".
1961 join( "\\tabularnewline\n~~",
1962 map _latex_escape($_), @{$line_item->{'ext_description'}}
1965 $invoice_data{'amount'} = $line_item->{'amount'};
1966 $invoice_data{'unit_amount'} = $line_item->{'unit_amount'};
1967 $invoice_data{'quantity'} = $line_item->{'quantity'};
1968 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1970 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1973 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1975 while ( ( my $total_item_line = shift @invoice_template )
1976 !~ /^%%EndTotalDetails\s*$/ ) {
1977 push @total_item, $total_item_line;
1980 my @total_fill = ();
1983 foreach my $tax ( $self->_items_tax ) {
1984 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1985 $taxtotal += $tax->{'amount'};
1986 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1988 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1993 $invoice_data{'total_item'} = 'Sub-total';
1994 $invoice_data{'total_amount'} =
1995 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1996 unshift @total_fill,
1997 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2001 $invoice_data{'total_item'} = '\textbf{Total}';
2002 $invoice_data{'total_amount'} =
2003 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2005 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2008 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2011 foreach my $credit ( $self->_items_credits ) {
2012 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2014 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2016 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2021 foreach my $payment ( $self->_items_payments ) {
2022 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2024 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2026 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2030 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2031 $invoice_data{'total_amount'} =
2032 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2034 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2037 push @filled_in, @total_fill;
2040 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2041 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2042 push @filled_in, $line;
2053 } elsif ( $format eq 'Text::Template' ) {
2055 my @detail_items = ();
2056 my @total_items = ();
2058 $invoice_data{'detail_items'} = \@detail_items;
2059 $invoice_data{'total_items'} = \@total_items;
2061 my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2062 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2064 ext_description => [],
2066 $detail->{'ref'} = $line_item->{'pkgnum'};
2067 $detail->{'quantity'} = 1;
2068 $detail->{'description'} = _latex_escape($line_item->{'description'});
2069 if ( exists $line_item->{'ext_description'} ) {
2070 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2072 $detail->{'amount'} = $line_item->{'amount'};
2073 $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2074 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2076 push @detail_items, $detail;
2081 foreach my $tax ( $self->_items_tax ) {
2083 $total->{'total_item'} = _latex_escape($tax->{'description'});
2084 $taxtotal += $tax->{'amount'};
2085 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2086 push @total_items, $total;
2090 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2092 $total->{'total_item'} = 'Sub-total';
2093 $total->{'total_amount'} =
2094 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2095 unshift @total_items, $total;
2097 $invoice_data{'taxtotal'} = '0.00';
2102 $total->{'total_item'} = '\textbf{Total}';
2103 $total->{'total_amount'} =
2106 $self->charged + ( $conf->exists('disable_previous_balance')
2112 push @total_items, $total;
2115 unless ($conf->exists('disable_previous_balance')) {
2116 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2119 my $credittotal = 0;
2120 foreach my $credit ( $self->_items_credits ) {
2122 $total->{'total_item'} = _latex_escape($credit->{'description'});
2123 $credittotal += $credit->{'amount'};
2124 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2125 push @total_items, $total;
2127 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2130 my $paymenttotal = 0;
2131 foreach my $payment ( $self->_items_payments ) {
2133 $total->{'total_item'} = _latex_escape($payment->{'description'});
2134 $paymenttotal += $payment->{'amount'};
2135 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2136 push @total_items, $total;
2138 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2142 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2143 $total->{'total_amount'} =
2144 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2145 push @total_items, $total;
2150 die "guru meditation #54";
2153 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2154 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2158 ) or die "can't open temp file: $!\n";
2159 if ( $format eq 'old' ) {
2160 print $fh join('', @filled_in );
2161 } elsif ( $format eq 'Text::Template' ) {
2162 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2164 die "guru meditation #32";
2168 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2173 =item print_ps [ TIME [ , TEMPLATE ] ]
2175 Returns an postscript invoice, as a scalar.
2177 TIME an optional value used to control the printing of overdue messages. The
2178 default is now. It isn't the date of the invoice; that's the `_date' field.
2179 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2180 L<Time::Local> and L<Date::Parse> for conversion functions.
2187 my $file = $self->print_latex(@_);
2188 my $ps = generate_ps($file);
2193 =item print_pdf [ TIME [ , TEMPLATE ] ]
2195 Returns an PDF invoice, as a scalar.
2197 TIME an optional value used to control the printing of overdue messages. The
2198 default is now. It isn't the date of the invoice; that's the `_date' field.
2199 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2200 L<Time::Local> and L<Date::Parse> for conversion functions.
2207 my $file = $self->print_latex(@_);
2208 my $pdf = generate_pdf($file);
2213 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2215 Returns an HTML invoice, as a scalar.
2217 TIME an optional value used to control the printing of overdue messages. The
2218 default is now. It isn't the date of the invoice; that's the `_date' field.
2219 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2220 L<Time::Local> and L<Date::Parse> for conversion functions.
2222 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2223 when emailing the invoice as part of a multipart/related MIME email.
2227 #some falze laziness w/print_text and print_latex (and send_csv)
2229 my( $self, $today, $template, $cid ) = @_;
2232 my $cust_main = $self->cust_main;
2233 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2234 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2236 $template ||= $self->_agent_template;
2237 my $templatefile = 'invoice_html';
2238 my $suffix = length($template) ? "_$template" : '';
2239 $templatefile .= $suffix;
2240 my @html_template = map "$_\n", $conf->config($templatefile)
2241 or die "cannot load config file $templatefile";
2243 my $html_template = new Text::Template(
2245 SOURCE => \@html_template,
2246 DELIMITERS => [ '<%=', '%>' ],
2249 $html_template->compile()
2250 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2252 my %invoice_data = (
2253 'custnum' => $self->custnum,
2254 'invnum' => $self->invnum,
2255 'date' => time2str('%b %o, %Y', $self->_date),
2256 'today' => time2str('%b %o, %Y', $today),
2257 'agent' => encode_entities($cust_main->agent->agent),
2258 'agent_custid' => encode_entities($cust_main->agent_custid),
2259 'payname' => encode_entities($cust_main->payname),
2260 'company' => encode_entities($cust_main->company),
2261 'address1' => encode_entities($cust_main->address1),
2262 'address2' => encode_entities($cust_main->address2),
2263 'city' => encode_entities($cust_main->city),
2264 'state' => encode_entities($cust_main->state),
2265 'zip' => encode_entities($cust_main->zip),
2266 'fax' => encode_entities($cust_main->fax),
2267 'terms' => $conf->config('invoice_default_terms')
2268 || 'Payable upon receipt',
2270 'template' => $template,
2271 'ship_enable' => $conf->exists('invoice-ship_address'),
2272 'unitprices' => $conf->exists('invoice-unitprice'),
2273 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2276 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2277 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2278 my $method = $prefix.$_;
2279 $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2283 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2284 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2286 $invoice_data{'returnaddress'} =
2287 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2289 $invoice_data{'returnaddress'} =
2292 s/\\\\\*?\s*$/<BR>/;
2293 s/\\hyphenation\{[\w\s\-]+\}//;
2297 $conf->config_orbase( 'invoice_latexreturnaddress',
2303 my $countrydefault = $conf->config('countrydefault') || 'US';
2304 if ( $cust_main->country eq $countrydefault ) {
2305 $invoice_data{'country'} = '';
2307 $invoice_data{'country'} =
2308 encode_entities(code2country($cust_main->country));
2312 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2313 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2315 $invoice_data{'notes'} =
2316 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2318 $invoice_data{'notes'} =
2320 s/%%(.*)$/<!-- $1 -->/g;
2321 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2322 s/\\begin\{enumerate\}/<ol>/g;
2324 s/\\end\{enumerate\}/<\/ol>/g;
2325 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2332 $conf->config_orbase('invoice_latexnotes', $template)
2336 # #do variable substitutions in notes
2337 # $invoice_data{'notes'} =
2339 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2340 # $conf->config_orbase('invoice_latexnotes', $suffix)
2344 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2345 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2347 $invoice_data{'footer'} =
2348 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2350 $invoice_data{'footer'} =
2351 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2352 $conf->config_orbase('invoice_latexfooter', $template)
2356 $invoice_data{'po_line'} =
2357 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2358 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2361 my $money_char = $conf->config('money_char') || '$';
2363 my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2364 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2366 ext_description => [],
2368 $detail->{'ref'} = $line_item->{'pkgnum'};
2369 $detail->{'description'} = encode_entities($line_item->{'description'});
2370 if ( exists $line_item->{'ext_description'} ) {
2371 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2373 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2374 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2376 push @{$invoice_data{'detail_items'}}, $detail;
2381 foreach my $tax ( $self->_items_tax ) {
2383 $total->{'total_item'} = encode_entities($tax->{'description'});
2384 $taxtotal += $tax->{'amount'};
2385 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2386 push @{$invoice_data{'total_items'}}, $total;
2391 $total->{'total_item'} = 'Sub-total';
2392 $total->{'total_amount'} =
2393 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2394 unshift @{$invoice_data{'total_items'}}, $total;
2397 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2400 $total->{'total_item'} = '<b>Total</b>';
2401 $total->{'total_amount'} =
2404 $self->charged + ( $conf->exists('disable_previous_balance')
2410 push @{$invoice_data{'total_items'}}, $total;
2413 unless ($conf->exists('disable_previous_balance')) {
2414 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2417 foreach my $credit ( $self->_items_credits ) {
2419 $total->{'total_item'} = encode_entities($credit->{'description'});
2421 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2422 push @{$invoice_data{'total_items'}}, $total;
2426 foreach my $payment ( $self->_items_payments ) {
2428 $total->{'total_item'} = encode_entities($payment->{'description'});
2430 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2431 push @{$invoice_data{'total_items'}}, $total;
2436 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2437 $total->{'total_amount'} =
2438 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2439 push @{$invoice_data{'total_items'}}, $total;
2443 $html_template->fill_in( HASH => \%invoice_data);
2446 # quick subroutine for print_latex
2448 # There are ten characters that LaTeX treats as special characters, which
2449 # means that they do not simply typeset themselves:
2450 # # $ % & ~ _ ^ \ { }
2452 # TeX ignores blanks following an escaped character; if you want a blank (as
2453 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2457 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2458 $value =~ s/([<>])/\$$1\$/g;
2462 #utility methods for print_*
2464 sub balance_due_msg {
2466 my $msg = 'Balance Due';
2467 return $msg unless $conf->exists('invoice_default_terms');
2468 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2469 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2470 } elsif ( $conf->config('invoice_default_terms') ) {
2471 $msg .= ' - '. $conf->config('invoice_default_terms');
2476 sub balance_due_date {
2479 if ( $conf->exists('invoice_default_terms')
2480 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2481 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2486 =item invnum_date_pretty
2488 Returns a string with the invoice number and date, for example:
2489 "Invoice #54 (3/20/2008)"
2493 sub invnum_date_pretty {
2495 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2500 Returns a string with the date, for example: "3/20/2008"
2506 time2str('%x', $self->_date);
2512 #my @display = scalar(@_)
2514 # : qw( _items_previous _items_pkg );
2515 # #: qw( _items_pkg );
2516 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2517 my @display = qw( _items_previous _items_pkg );
2520 foreach my $display ( @display ) {
2521 push @b, $self->$display(@_);
2526 sub _items_previous {
2528 my $cust_main = $self->cust_main;
2529 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2531 foreach ( @pr_cust_bill ) {
2533 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2534 ' ('. time2str('%x',$_->_date). ')',
2535 #'pkgpart' => 'N/A',
2537 'amount' => sprintf("%.2f", $_->owed),
2543 # 'description' => 'Previous Balance',
2544 # #'pkgpart' => 'N/A',
2545 # 'pkgnum' => 'N/A',
2546 # 'amount' => sprintf("%10.2f", $pr_total ),
2547 # 'ext_description' => [ map {
2548 # "Invoice ". $_->invnum.
2549 # " (". time2str("%x",$_->_date). ") ".
2550 # sprintf("%10.2f", $_->owed)
2551 # } @pr_cust_bill ],
2558 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2559 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2564 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2565 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2568 sub _items_cust_bill_pkg {
2570 my $cust_bill_pkg = shift;
2573 my $format = $opt{format} || '';
2574 my $escape_function = $opt{escape_function} || sub { shift };
2577 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2579 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2581 my $desc = $cust_bill_pkg->desc;
2582 $desc = substr($desc, 0, 50). '...'
2583 if $format eq 'latex' && length($desc) > 50;
2585 my %details_opt = ( 'format' => $format,
2586 'escape_function' => $escape_function,
2589 if ( $cust_bill_pkg->pkgnum > 0 ) {
2591 if ( $cust_bill_pkg->setup != 0 ) {
2593 my $description = $desc;
2594 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2597 push @d, map &{$escape_function}($_),
2598 $cust_pkg->h_labels_short($self->_date)
2599 unless $cust_pkg->part_pkg->hide_svc_detail;
2601 push @d, $cust_bill_pkg->details(%details_opt)
2602 if $cust_bill_pkg->recur == 0;
2605 description => $description,
2606 #pkgpart => $part_pkg->pkgpart,
2607 pkgnum => $cust_bill_pkg->pkgnum,
2608 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2609 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2610 quantity => $cust_bill_pkg->quantity,
2611 ext_description => \@d,
2615 if ( $cust_bill_pkg->recur != 0 ) {
2617 my $description = $desc;
2618 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2619 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2620 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2625 #at least until cust_bill_pkg has "past" ranges in addition to
2626 #the "future" sdate/edate ones... see #3032
2627 push @d, map &{$escape_function}($_),
2628 $cust_pkg->h_labels_short($self->_date)
2629 #$cust_bill_pkg->edate,
2630 #$cust_bill_pkg->sdate),
2631 unless $cust_pkg->part_pkg->hide_svc_detail
2632 || $cust_bill_pkg->itemdesc;
2634 push @d, $cust_bill_pkg->details(%details_opt);
2637 description => $description,
2638 #pkgpart => $part_pkg->pkgpart,
2639 pkgnum => $cust_bill_pkg->pkgnum,
2640 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2641 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2642 quantity => $cust_bill_pkg->quantity,
2643 ext_description => \@d,
2648 } else { #pkgnum tax or one-shot line item (??)
2650 if ( $cust_bill_pkg->setup != 0 ) {
2652 'description' => $desc,
2653 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2656 if ( $cust_bill_pkg->recur != 0 ) {
2658 'description' => "$desc (".
2659 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2660 time2str("%x", $cust_bill_pkg->edate). ')',
2661 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2673 sub _items_credits {
2678 foreach ( $self->cust_credited ) {
2680 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2682 my $reason = $_->cust_credit->reason;
2683 #my $reason = substr($_->cust_credit->reason,0,32);
2684 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2685 $reason = " ($reason) " if $reason;
2687 #'description' => 'Credit ref\#'. $_->crednum.
2688 # " (". time2str("%x",$_->cust_credit->_date) .")".
2690 'description' => 'Credit applied '.
2691 time2str("%x",$_->cust_credit->_date). $reason,
2692 'amount' => sprintf("%.2f",$_->amount),
2695 #foreach ( @cr_cust_credit ) {
2697 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2698 # $money_char. sprintf("%10.2f",$_->credited)
2706 sub _items_payments {
2710 #get & print payments
2711 foreach ( $self->cust_bill_pay ) {
2713 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2716 'description' => "Payment received ".
2717 time2str("%x",$_->cust_pay->_date ),
2718 'amount' => sprintf("%.2f", $_->amount )
2733 =item process_reprint
2737 sub process_reprint {
2738 process_re_X('print', @_);
2741 =item process_reemail
2745 sub process_reemail {
2746 process_re_X('email', @_);
2754 process_re_X('fax', @_);
2762 process_re_X('ftp', @_);
2769 sub process_respool {
2770 process_re_X('spool', @_);
2773 use Storable qw(thaw);
2777 my( $method, $job ) = ( shift, shift );
2778 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2780 my $param = thaw(decode_base64(shift));
2781 warn Dumper($param) if $DEBUG;
2792 my($method, $job, %param ) = @_;
2794 warn "re_X $method for job $job with param:\n".
2795 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2798 #some false laziness w/search/cust_bill.html
2800 my $orderby = 'ORDER BY cust_bill._date';
2802 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2804 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2806 my @cust_bill = qsearch( {
2807 #'select' => "cust_bill.*",
2808 'table' => 'cust_bill',
2809 'addl_from' => $addl_from,
2811 'extra_sql' => $extra_sql,
2812 'order_by' => $orderby,
2816 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2818 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2821 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2822 foreach my $cust_bill ( @cust_bill ) {
2823 $cust_bill->$method();
2825 if ( $job ) { #progressbar foo
2827 if ( time - $min_sec > $last ) {
2828 my $error = $job->update_statustext(
2829 int( 100 * $num / scalar(@cust_bill) )
2831 die $error if $error;
2842 =head1 CLASS METHODS
2848 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2854 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2859 Returns an SQL fragment to retreive the net amount (charged minus credited).
2865 'charged - '. $class->credited_sql;
2870 Returns an SQL fragment to retreive the amount paid against this invoice.
2876 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2877 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2882 Returns an SQL fragment to retreive the amount credited against this invoice.
2888 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2889 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2892 =item search_sql HASHREF
2894 Class method which returns an SQL WHERE fragment to search for parameters
2895 specified in HASHREF. Valid parameters are
2901 Epoch date (UNIX timestamp) setting a lower bound for _date values
2905 Epoch date (UNIX timestamp) setting an upper bound for _date values
2919 =item newest_percust
2923 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2928 my($class, $param) = @_;
2930 warn "$me search_sql called with params: \n".
2931 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2936 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2937 push @search, "cust_bill._date >= $1";
2939 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2940 push @search, "cust_bill._date < $1";
2942 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2943 push @search, "cust_bill.invnum >= $1";
2945 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2946 push @search, "cust_bill.invnum <= $1";
2948 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2949 push @search, "cust_main.agentnum = $1";
2952 push @search, '0 != '. FS::cust_bill->owed_sql
2953 if $param->{'open'};
2955 push @search, '0 != '. FS::cust_bill->net_sql
2958 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2959 if $param->{'days'};
2961 if ( $param->{'newest_percust'} ) {
2963 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2964 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2966 my @newest_where = map { my $x = $_;
2967 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2970 grep ! /^cust_main./, @search;
2971 my $newest_where = scalar(@newest_where)
2972 ? ' AND '. join(' AND ', @newest_where)
2976 push @search, "cust_bill._date = (
2977 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2978 WHERE newest_cust_bill.custnum = cust_bill.custnum
2984 my $curuser = $FS::CurrentUser::CurrentUser;
2985 if ( $curuser->username eq 'fs_queue'
2986 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2988 my $newuser = qsearchs('access_user', {
2989 'username' => $username,
2993 $curuser = $newuser;
2995 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2999 push @search, $curuser->agentnums_sql;
3001 join(' AND ', @search );
3013 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3014 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base