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 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.
758 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
759 or die "invalid invoice number: " . $opt{invnum};
761 my @args = ( $opt{template}, $opt{agentnum} );
762 push @args, $opt{invoice_from}
763 if exists($opt{invoice_from}) && $opt{invoice_from};
765 my $error = $self->send( @args );
766 die $error if $error;
772 my $template = scalar(@_) ? shift : '';
773 if ( scalar(@_) && $_[0] ) {
774 my $agentnums = ref($_[0]) ? shift : [ shift ];
775 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
781 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
783 my @invoicing_list = $self->cust_main->invoicing_list;
785 $self->email($template, $invoice_from)
786 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
788 $self->print($template)
789 if grep { $_ eq 'POST' } @invoicing_list; #postal
791 $self->fax($template)
792 if grep { $_ eq 'FAX' } @invoicing_list; #fax
798 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
802 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
804 INVOICE_FROM, if specified, overrides the default email invoice From: address.
808 sub queueable_email {
811 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
812 or die "invalid invoice number: " . $opt{invnum};
814 my @args = ( $opt{template} );
815 push @args, $opt{invoice_from}
816 if exists($opt{invoice_from}) && $opt{invoice_from};
818 my $error = $self->email( @args );
819 die $error if $error;
825 my $template = scalar(@_) ? shift : '';
829 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
831 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
832 $self->cust_main->invoicing_list;
834 #better to notify this person than silence
835 @invoicing_list = ($invoice_from) unless @invoicing_list;
837 my $error = send_email(
838 $self->generate_email(
839 'from' => $invoice_from,
840 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
841 'template' => $template,
844 die "can't email invoice: $error\n" if $error;
845 #die "$error\n" if $error;
849 =item lpr_data [ TEMPLATENAME ]
851 Returns the postscript or plaintext for this invoice as an arrayref.
853 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
858 my( $self, $template) = @_;
859 $conf->exists('invoice_latex')
860 ? [ $self->print_ps('', $template) ]
861 : [ $self->print_text('', $template) ];
864 =item print [ TEMPLATENAME ]
868 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
874 my $template = scalar(@_) ? shift : '';
876 do_print $self->lpr_data($template);
879 =item fax [ TEMPLATENAME ]
883 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
889 my $template = scalar(@_) ? shift : '';
891 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
892 unless $conf->exists('invoice_latex');
894 my $dialstring = $self->cust_main->getfield('fax');
897 my $error = send_fax( 'docdata' => $self->lpr_data($template),
898 'dialstring' => $dialstring,
900 die $error if $error;
904 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
906 Like B<send>, but only sends the invoice if it is the newest open invoice for
916 grep { $_->owed > 0 }
917 qsearch('cust_bill', {
918 'custnum' => $self->custnum,
919 #'_date' => { op=>'>', value=>$self->_date },
920 'invnum' => { op=>'>', value=>$self->invnum },
927 =item send_csv OPTION => VALUE, ...
929 Sends invoice as a CSV data-file to a remote host with the specified protocol.
933 protocol - currently only "ftp"
939 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
940 and YYMMDDHHMMSS is a timestamp.
942 See L</print_csv> for a description of the output format.
947 my($self, %opt) = @_;
951 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
952 mkdir $spooldir, 0700 unless -d $spooldir;
954 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
955 my $file = "$spooldir/$tracctnum.csv";
957 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
959 open(CSV, ">$file") or die "can't open $file: $!";
967 if ( $opt{protocol} eq 'ftp' ) {
968 eval "use Net::FTP;";
970 $net = Net::FTP->new($opt{server}) or die @$;
972 die "unknown protocol: $opt{protocol}";
975 $net->login( $opt{username}, $opt{password} )
976 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
978 $net->binary or die "can't set binary mode";
980 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
982 $net->put($file) or die "can't put $file: $!";
992 Spools CSV invoice data.
998 =item format - 'default' or 'billco'
1000 =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>).
1002 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1004 =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.
1011 my($self, %opt) = @_;
1013 my $cust_main = $self->cust_main;
1015 if ( $opt{'dest'} ) {
1016 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1017 $cust_main->invoicing_list;
1018 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1019 || ! keys %invoicing_list;
1022 if ( $opt{'balanceover'} ) {
1024 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1027 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1028 mkdir $spooldir, 0700 unless -d $spooldir;
1030 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1034 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1035 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1038 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1040 open(CSV, ">>$file") or die "can't open $file: $!";
1041 flock(CSV, LOCK_EX);
1046 if ( lc($opt{'format'}) eq 'billco' ) {
1048 flock(CSV, LOCK_UN);
1053 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1056 open(CSV,">>$file") or die "can't open $file: $!";
1057 flock(CSV, LOCK_EX);
1063 flock(CSV, LOCK_UN);
1070 =item print_csv OPTION => VALUE, ...
1072 Returns CSV data for this invoice.
1076 format - 'default' or 'billco'
1078 Returns a list consisting of two scalars. The first is a single line of CSV
1079 header information for this invoice. The second is one or more lines of CSV
1080 detail information for this invoice.
1082 If I<format> is not specified or "default", the fields of the CSV file are as
1085 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1089 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1091 B<record_type> is C<cust_bill> for the initial header line only. The
1092 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1093 fields are filled in.
1095 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1096 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1099 =item invnum - invoice number
1101 =item custnum - customer number
1103 =item _date - invoice date
1105 =item charged - total invoice amount
1107 =item first - customer first name
1109 =item last - customer first name
1111 =item company - company name
1113 =item address1 - address line 1
1115 =item address2 - address line 1
1125 =item pkg - line item description
1127 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1129 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1131 =item sdate - start date for recurring fee
1133 =item edate - end date for recurring fee
1137 If I<format> is "billco", the fields of the header CSV file are as follows:
1139 +-------------------------------------------------------------------+
1140 | FORMAT HEADER FILE |
1141 |-------------------------------------------------------------------|
1142 | Field | Description | Name | Type | Width |
1143 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1144 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1145 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1146 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1147 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1148 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1149 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1150 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1151 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1152 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1153 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1154 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1155 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1156 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1157 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1158 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1159 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1160 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1161 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1162 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1163 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1164 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1165 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1166 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1167 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1168 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1169 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1170 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1171 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1172 +-------+-------------------------------+------------+------+-------+
1174 If I<format> is "billco", the fields of the detail CSV file are as follows:
1176 FORMAT FOR DETAIL FILE
1178 Field | Description | Name | Type | Width
1179 1 | N/A-Leave Empty | RC | CHAR | 2
1180 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1181 3 | Account Number | TRACCTNUM | CHAR | 15
1182 4 | Invoice Number | TRINVOICE | CHAR | 15
1183 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1184 6 | Transaction Detail | DETAILS | CHAR | 100
1185 7 | Amount | AMT | NUM* | 9
1186 8 | Line Format Control** | LNCTRL | CHAR | 2
1187 9 | Grouping Code | GROUP | CHAR | 2
1188 10 | User Defined | ACCT CODE | CHAR | 15
1193 my($self, %opt) = @_;
1195 eval "use Text::CSV_XS";
1198 my $cust_main = $self->cust_main;
1200 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1202 if ( lc($opt{'format'}) eq 'billco' ) {
1205 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1208 if ( $conf->exists('invoice_default_terms')
1209 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1210 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1213 my( $previous_balance, @unused ) = $self->previous; #previous balance
1215 my $pmt_cr_applied = 0;
1216 $pmt_cr_applied += $_->{'amount'}
1217 foreach ( $self->_items_payments, $self->_items_credits ) ;
1219 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1222 '', # 1 | N/A-Leave Empty CHAR 2
1223 '', # 2 | N/A-Leave Empty CHAR 15
1224 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1225 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1226 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1227 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1228 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1229 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1230 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1231 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1232 '', # 10 | Ancillary Billing Information CHAR 30
1233 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1234 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1237 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1240 $duedate, # 14 | Bill Due Date CHAR 10
1242 $previous_balance, # 15 | Previous Balance NUM* 9
1243 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1244 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1245 $totaldue, # 18 | Total Amt Due NUM* 9
1246 $totaldue, # 19 | Total Amt Due NUM* 9
1247 '', # 20 | 30 Day Aging NUM* 9
1248 '', # 21 | 60 Day Aging NUM* 9
1249 '', # 22 | 90 Day Aging NUM* 9
1250 'N', # 23 | Y/N CHAR 1
1251 '', # 24 | Remittance automation CHAR 100
1252 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1253 $self->custnum, # 26 | Customer Reference Number CHAR 15
1254 '0', # 27 | Federal Tax*** NUM* 9
1255 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1256 '0', # 29 | Other Taxes & Fees*** NUM* 9
1265 time2str("%x", $self->_date),
1266 sprintf("%.2f", $self->charged),
1267 ( map { $cust_main->getfield($_) }
1268 qw( first last company address1 address2 city state zip country ) ),
1270 ) or die "can't create csv";
1273 my $header = $csv->string. "\n";
1276 if ( lc($opt{'format'}) eq 'billco' ) {
1279 foreach my $item ( $self->_items_pkg ) {
1282 '', # 1 | N/A-Leave Empty CHAR 2
1283 '', # 2 | N/A-Leave Empty CHAR 15
1284 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1285 $self->invnum, # 4 | Invoice Number CHAR 15
1286 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1287 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1288 $item->{'amount'}, # 7 | Amount NUM* 9
1289 '', # 8 | Line Format Control** CHAR 2
1290 '', # 9 | Grouping Code CHAR 2
1291 '', # 10 | User Defined CHAR 15
1294 $detail .= $csv->string. "\n";
1300 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1302 my($pkg, $setup, $recur, $sdate, $edate);
1303 if ( $cust_bill_pkg->pkgnum ) {
1305 ($pkg, $setup, $recur, $sdate, $edate) = (
1306 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1307 ( $cust_bill_pkg->setup != 0
1308 ? sprintf("%.2f", $cust_bill_pkg->setup )
1310 ( $cust_bill_pkg->recur != 0
1311 ? sprintf("%.2f", $cust_bill_pkg->recur )
1313 ( $cust_bill_pkg->sdate
1314 ? time2str("%x", $cust_bill_pkg->sdate)
1316 ($cust_bill_pkg->edate
1317 ?time2str("%x", $cust_bill_pkg->edate)
1321 } else { #pkgnum tax
1322 next unless $cust_bill_pkg->setup != 0;
1323 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1324 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1326 ($pkg, $setup, $recur, $sdate, $edate) =
1327 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1333 ( map { '' } (1..11) ),
1334 ($pkg, $setup, $recur, $sdate, $edate)
1335 ) or die "can't create csv";
1337 $detail .= $csv->string. "\n";
1343 ( $header, $detail );
1349 Pays this invoice with a compliemntary payment. If there is an error,
1350 returns the error, otherwise returns false.
1356 my $cust_pay = new FS::cust_pay ( {
1357 'invnum' => $self->invnum,
1358 'paid' => $self->owed,
1361 'payinfo' => $self->cust_main->payinfo,
1369 Attempts to pay this invoice with a credit card payment via a
1370 Business::OnlinePayment realtime gateway. See
1371 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1372 for supported processors.
1378 $self->realtime_bop( 'CC', @_ );
1383 Attempts to pay this invoice with an electronic check (ACH) payment via a
1384 Business::OnlinePayment realtime gateway. See
1385 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1386 for supported processors.
1392 $self->realtime_bop( 'ECHECK', @_ );
1397 Attempts to pay this invoice with phone bill (LEC) payment via a
1398 Business::OnlinePayment realtime gateway. See
1399 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1400 for supported processors.
1406 $self->realtime_bop( 'LEC', @_ );
1410 my( $self, $method ) = @_;
1412 my $cust_main = $self->cust_main;
1413 my $balance = $cust_main->balance;
1414 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1415 $amount = sprintf("%.2f", $amount);
1416 return "not run (balance $balance)" unless $amount > 0;
1418 my $description = 'Internet Services';
1419 if ( $conf->exists('business-onlinepayment-description') ) {
1420 my $dtempl = $conf->config('business-onlinepayment-description');
1422 my $agent_obj = $cust_main->agent
1423 or die "can't retreive agent for $cust_main (agentnum ".
1424 $cust_main->agentnum. ")";
1425 my $agent = $agent_obj->agent;
1426 my $pkgs = join(', ',
1427 map { $_->cust_pkg->part_pkg->pkg }
1428 grep { $_->pkgnum } $self->cust_bill_pkg
1430 $description = eval qq("$dtempl");
1433 $cust_main->realtime_bop($method, $amount,
1434 'description' => $description,
1435 'invnum' => $self->invnum,
1440 =item batch_card OPTION => VALUE...
1442 Adds a payment for this invoice to the pending credit card batch (see
1443 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1444 runs the payment using a realtime gateway.
1449 my ($self, %options) = @_;
1450 my $cust_main = $self->cust_main;
1452 $options{invnum} = $self->invnum;
1454 $cust_main->batch_card(%options);
1457 sub _agent_template {
1459 $self->cust_main->agent_template;
1462 sub _agent_invoice_from {
1464 $self->cust_main->agent_invoice_from;
1467 =item print_text [ TIME [ , TEMPLATE ] ]
1469 Returns an text invoice, as a list of lines.
1471 TIME an optional value used to control the printing of overdue messages. The
1472 default is now. It isn't the date of the invoice; that's the `_date' field.
1473 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1474 L<Time::Local> and L<Date::Parse> for conversion functions.
1478 #still some false laziness w/_items stuff (and send_csv)
1481 my( $self, $today, $template ) = @_;
1484 # my $invnum = $self->invnum;
1485 my $cust_main = $self->cust_main;
1486 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1487 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1489 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1490 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1491 #my $balance_due = $self->owed + $pr_total - $cr_total;
1492 my $balance_due = $self->owed + $pr_total;
1495 #my($description,$amount);
1499 unless ($conf->exists('disable_previous_balance')) {
1500 foreach ( @pr_cust_bill ) {
1502 "Previous Balance, Invoice #". $_->invnum.
1503 " (". time2str("%x",$_->_date). ")",
1504 $money_char. sprintf("%10.2f",$_->owed)
1507 if (@pr_cust_bill) {
1508 push @buf,['','-----------'];
1509 push @buf,[ 'Total Previous Balance',
1510 $money_char. sprintf("%10.2f",$pr_total ) ];
1516 foreach my $cust_bill_pkg (
1517 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1518 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1521 my $desc = $cust_bill_pkg->desc;
1523 if ( $cust_bill_pkg->pkgnum > 0 ) {
1525 if ( $cust_bill_pkg->setup != 0 ) {
1526 my $description = $desc;
1527 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1528 push @buf, [ $description,
1529 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1531 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1532 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1535 if ( $cust_bill_pkg->recur != 0 ) {
1538 ( $conf->exists('disable_line_item_date_ranges')
1540 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1541 time2str("%x", $cust_bill_pkg->edate) . ")"
1543 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1546 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1547 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1548 $cust_bill_pkg->sdate );
1551 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1553 } else { #pkgnum tax or one-shot line item
1555 if ( $cust_bill_pkg->setup != 0 ) {
1557 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1559 if ( $cust_bill_pkg->recur != 0 ) {
1560 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1561 . time2str("%x", $cust_bill_pkg->edate). ")",
1562 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1570 push @buf,['','-----------'];
1571 push @buf,[ ( $conf->exists('disable_previous_balance')
1573 : 'Total New Charges'),
1574 $money_char. sprintf("%10.2f",$self->charged) ];
1577 unless ($conf->exists('disable_previous_balance')) {
1578 push @buf,['','-----------'];
1579 push @buf,['Total Charges',
1580 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1584 foreach ( $self->cust_credited ) {
1586 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1588 my $reason = substr($_->cust_credit->reason,0,32);
1589 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1590 $reason = " ($reason) " if $reason;
1592 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1594 $money_char. sprintf("%10.2f",$_->amount)
1597 #foreach ( @cr_cust_credit ) {
1599 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1600 # $money_char. sprintf("%10.2f",$_->credited)
1604 #get & print payments
1605 foreach ( $self->cust_bill_pay ) {
1607 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1610 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1611 $money_char. sprintf("%10.2f",$_->amount )
1616 my $balance_due_msg = $self->balance_due_msg;
1618 push @buf,['','-----------'];
1619 push @buf,[$balance_due_msg, $money_char.
1620 sprintf("%10.2f", $balance_due ) ];
1623 #create the template
1624 $template ||= $self->_agent_template;
1625 my $templatefile = 'invoice_template';
1626 $templatefile .= "_$template" if length($template);
1627 my @invoice_template = $conf->config($templatefile)
1628 or die "cannot load config file $templatefile";
1631 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1632 /invoice_lines\((\d*)\)/;
1633 $invoice_lines += $1 || scalar(@buf);
1636 die "no invoice_lines() functions in template?" unless $wasfunc;
1637 my $invoice_template = new Text::Template (
1639 SOURCE => [ map "$_\n", @invoice_template ],
1640 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1641 $invoice_template->compile()
1642 or die "can't compile template: $Text::Template::ERROR";
1644 #setup template variables
1645 package FS::cust_bill::_template; #!
1646 use vars qw( $custnum $invnum $date $agent @address $overdue
1647 $page $total_pages @buf );
1649 $custnum = $self->custnum;
1650 $invnum = $self->invnum;
1651 $date = $self->_date;
1652 $agent = $self->cust_main->agent->agent;
1655 if ( $FS::cust_bill::invoice_lines ) {
1657 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1659 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1664 #format address (variable for the template)
1666 @address = ( '', '', '', '', '', '' );
1667 package FS::cust_bill; #!
1668 $FS::cust_bill::_template::address[$l++] =
1669 $cust_main->payname.
1670 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1671 ? " (P.O. #". $cust_main->payinfo. ")"
1675 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1676 if $cust_main->company;
1677 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1678 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1679 if $cust_main->address2;
1680 $FS::cust_bill::_template::address[$l++] =
1681 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1683 my $countrydefault = $conf->config('countrydefault') || 'US';
1684 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1685 unless $cust_main->country eq $countrydefault;
1687 # #overdue? (variable for the template)
1688 # $FS::cust_bill::_template::overdue = (
1690 # && $today > $self->_date
1691 ## && $self->printed > 1
1692 # && $self->printed > 0
1695 #and subroutine for the template
1696 sub FS::cust_bill::_template::invoice_lines {
1697 my $lines = shift || scalar(@buf);
1699 scalar(@buf) ? shift @buf : [ '', '' ];
1705 $FS::cust_bill::_template::page = 1;
1709 push @collect, split("\n",
1710 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1712 $FS::cust_bill::_template::page++;
1715 map "$_\n", @collect;
1719 =item print_latex [ TIME [ , TEMPLATE ] ]
1721 Internal method - returns a filename of a filled-in LaTeX template for this
1722 invoice (Note: add ".tex" to get the actual filename).
1724 See print_ps and print_pdf for methods that return PostScript and PDF output.
1726 TIME an optional value used to control the printing of overdue messages. The
1727 default is now. It isn't the date of the invoice; that's the `_date' field.
1728 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1729 L<Time::Local> and L<Date::Parse> for conversion functions.
1733 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1736 my( $self, $today, $template ) = @_;
1738 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1741 my $cust_main = $self->cust_main;
1742 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1743 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1745 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1746 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1747 #my $balance_due = $self->owed + $pr_total - $cr_total;
1748 my $balance_due = $self->owed + $pr_total;
1750 #create the template
1751 $template ||= $self->_agent_template;
1752 my $templatefile = 'invoice_latex';
1753 my $suffix = length($template) ? "_$template" : '';
1754 $templatefile .= $suffix;
1755 my @invoice_template = map "$_\n", $conf->config($templatefile)
1756 or die "cannot load config file $templatefile";
1758 my($format, $text_template);
1759 if ( grep { /^%%Detail/ } @invoice_template ) {
1760 #change this to a die when the old code is removed
1761 warn "old-style invoice template $templatefile; ".
1762 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1765 $format = 'Text::Template';
1766 $text_template = new Text::Template(
1768 SOURCE => \@invoice_template,
1769 DELIMITERS => [ '[@--', '--@]' ],
1772 $text_template->compile()
1773 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1777 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1778 $returnaddress = join("\n",
1779 $conf->config_orbase('invoice_latexreturnaddress', $template)
1782 $returnaddress = '~';
1785 my %invoice_data = (
1786 'custnum' => $self->custnum,
1787 'invnum' => $self->invnum,
1788 'date' => time2str('%b %o, %Y', $self->_date),
1789 'today' => time2str('%b %o, %Y', $today),
1790 'agent' => _latex_escape($cust_main->agent->agent),
1791 'payname' => _latex_escape($cust_main->payname),
1792 'company' => _latex_escape($cust_main->company),
1793 'address1' => _latex_escape($cust_main->address1),
1794 'address2' => _latex_escape($cust_main->address2),
1795 'city' => _latex_escape($cust_main->city),
1796 'state' => _latex_escape($cust_main->state),
1797 'zip' => _latex_escape($cust_main->zip),
1798 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1799 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1800 'returnaddress' => $returnaddress,
1802 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1803 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1804 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1807 my $countrydefault = $conf->config('countrydefault') || 'US';
1808 if ( $cust_main->country eq $countrydefault ) {
1809 $invoice_data{'country'} = '';
1811 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1814 $invoice_data{'notes'} =
1816 # #do variable substitutions in notes
1817 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1818 $conf->config_orbase('invoice_latexnotes', $template)
1820 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1823 $invoice_data{'footer'} =~ s/\n+$//;
1824 $invoice_data{'smallfooter'} =~ s/\n+$//;
1825 $invoice_data{'notes'} =~ s/\n+$//;
1827 $invoice_data{'po_line'} =
1828 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1829 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1833 if ( $format eq 'old' ) {
1836 my @total_item = ();
1837 while ( @invoice_template ) {
1838 my $line = shift @invoice_template;
1840 if ( $line =~ /^%%Detail\s*$/ ) {
1842 while ( ( my $line_item_line = shift @invoice_template )
1843 !~ /^%%EndDetail\s*$/ ) {
1844 push @line_item, $line_item_line;
1846 foreach my $line_item ( $self->_items ) {
1847 #foreach my $line_item ( $self->_items_pkg ) {
1848 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1849 $invoice_data{'description'} =
1850 _latex_escape($line_item->{'description'});
1851 if ( exists $line_item->{'ext_description'} ) {
1852 $invoice_data{'description'} .=
1853 "\\tabularnewline\n~~".
1854 join( "\\tabularnewline\n~~",
1855 map _latex_escape($_), @{$line_item->{'ext_description'}}
1858 $invoice_data{'amount'} = $line_item->{'amount'};
1859 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1861 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1864 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1866 while ( ( my $total_item_line = shift @invoice_template )
1867 !~ /^%%EndTotalDetails\s*$/ ) {
1868 push @total_item, $total_item_line;
1871 my @total_fill = ();
1874 foreach my $tax ( $self->_items_tax ) {
1875 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1876 $taxtotal += $tax->{'amount'};
1877 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1879 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1884 $invoice_data{'total_item'} = 'Sub-total';
1885 $invoice_data{'total_amount'} =
1886 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1887 unshift @total_fill,
1888 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1892 $invoice_data{'total_item'} = '\textbf{Total}';
1893 $invoice_data{'total_amount'} =
1894 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1896 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1899 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1902 foreach my $credit ( $self->_items_credits ) {
1903 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1905 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1907 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1912 foreach my $payment ( $self->_items_payments ) {
1913 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1915 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1917 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1921 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1922 $invoice_data{'total_amount'} =
1923 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1925 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1928 push @filled_in, @total_fill;
1931 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1932 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1933 push @filled_in, $line;
1944 } elsif ( $format eq 'Text::Template' ) {
1946 my @detail_items = ();
1947 my @total_items = ();
1949 $invoice_data{'detail_items'} = \@detail_items;
1950 $invoice_data{'total_items'} = \@total_items;
1952 foreach my $line_item ( $self->_items($conf->exists('disable_previous_balance') ? qw( _items_pkg ) : () ) ) {
1954 ext_description => [],
1956 $detail->{'ref'} = $line_item->{'pkgnum'};
1957 $detail->{'quantity'} = 1;
1958 $detail->{'description'} = _latex_escape($line_item->{'description'});
1959 if ( exists $line_item->{'ext_description'} ) {
1960 @{$detail->{'ext_description'}} = map {
1962 } @{$line_item->{'ext_description'}};
1964 $detail->{'amount'} = $line_item->{'amount'};
1965 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1967 push @detail_items, $detail;
1972 foreach my $tax ( $self->_items_tax ) {
1974 $total->{'total_item'} = _latex_escape($tax->{'description'});
1975 $taxtotal += $tax->{'amount'};
1976 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1977 push @total_items, $total;
1982 $total->{'total_item'} = 'Sub-total';
1983 $total->{'total_amount'} =
1984 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1985 unshift @total_items, $total;
1990 $total->{'total_item'} = '\textbf{Total}';
1991 $total->{'total_amount'} =
1994 $self->charged + ( $conf->exists('disable_previous_balance')
2000 push @total_items, $total;
2003 unless ($conf->exists('disable_previous_balance')) {
2004 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2007 foreach my $credit ( $self->_items_credits ) {
2009 $total->{'total_item'} = _latex_escape($credit->{'description'});
2011 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2012 push @total_items, $total;
2016 foreach my $payment ( $self->_items_payments ) {
2018 $total->{'total_item'} = _latex_escape($payment->{'description'});
2020 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2021 push @total_items, $total;
2026 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2027 $total->{'total_amount'} =
2028 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2029 push @total_items, $total;
2034 die "guru meditation #54";
2037 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2038 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2042 ) or die "can't open temp file: $!\n";
2043 if ( $format eq 'old' ) {
2044 print $fh join('', @filled_in );
2045 } elsif ( $format eq 'Text::Template' ) {
2046 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2048 die "guru meditation #32";
2052 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2057 =item print_ps [ TIME [ , TEMPLATE ] ]
2059 Returns an postscript invoice, as a scalar.
2061 TIME an optional value used to control the printing of overdue messages. The
2062 default is now. It isn't the date of the invoice; that's the `_date' field.
2063 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2064 L<Time::Local> and L<Date::Parse> for conversion functions.
2071 my $file = $self->print_latex(@_);
2072 FS::Misc::generate_ps($file);
2076 =item print_pdf [ TIME [ , TEMPLATE ] ]
2078 Returns an PDF invoice, as a scalar.
2080 TIME an optional value used to control the printing of overdue messages. The
2081 default is now. It isn't the date of the invoice; that's the `_date' field.
2082 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2083 L<Time::Local> and L<Date::Parse> for conversion functions.
2090 my $file = $self->print_latex(@_);
2092 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2095 #system('pdflatex', "$file.tex");
2096 #system('pdflatex', "$file.tex");
2097 #! LaTeX Error: Unknown graphics extension: .eps.
2099 my $sfile = shell_quote $file;
2101 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2102 or die "pslatex $file.tex failed; see $file.log for details?\n";
2103 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2104 or die "pslatex $file.tex failed; see $file.log for details?\n";
2106 #system('dvipdf', "$file.dvi", "$file.pdf" );
2108 "dvips -q -t letter -f $sfile.dvi ".
2109 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2112 or die "dvips | gs failed: $!";
2114 open(PDF, "<$file.pdf")
2115 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2117 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2130 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2132 Returns an HTML invoice, as a scalar.
2134 TIME an optional value used to control the printing of overdue messages. The
2135 default is now. It isn't the date of the invoice; that's the `_date' field.
2136 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2137 L<Time::Local> and L<Date::Parse> for conversion functions.
2139 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2140 when emailing the invoice as part of a multipart/related MIME email.
2144 #some falze laziness w/print_text and print_latex (and send_csv)
2146 my( $self, $today, $template, $cid ) = @_;
2149 my $cust_main = $self->cust_main;
2150 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2151 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2153 $template ||= $self->_agent_template;
2154 my $templatefile = 'invoice_html';
2155 my $suffix = length($template) ? "_$template" : '';
2156 $templatefile .= $suffix;
2157 my @html_template = map "$_\n", $conf->config($templatefile)
2158 or die "cannot load config file $templatefile";
2160 my $html_template = new Text::Template(
2162 SOURCE => \@html_template,
2163 DELIMITERS => [ '<%=', '%>' ],
2166 $html_template->compile()
2167 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2169 my %invoice_data = (
2170 'custnum' => $self->custnum,
2171 'invnum' => $self->invnum,
2172 'date' => time2str('%b %o, %Y', $self->_date),
2173 'today' => time2str('%b %o, %Y', $today),
2174 'agent' => encode_entities($cust_main->agent->agent),
2175 'payname' => encode_entities($cust_main->payname),
2176 'company' => encode_entities($cust_main->company),
2177 'address1' => encode_entities($cust_main->address1),
2178 'address2' => encode_entities($cust_main->address2),
2179 'city' => encode_entities($cust_main->city),
2180 'state' => encode_entities($cust_main->state),
2181 'zip' => encode_entities($cust_main->zip),
2182 'terms' => $conf->config('invoice_default_terms')
2183 || 'Payable upon receipt',
2185 'template' => $template,
2186 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2190 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2191 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2193 $invoice_data{'returnaddress'} =
2194 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2196 $invoice_data{'returnaddress'} =
2199 s/\\\\\*?\s*$/<BR>/;
2200 s/\\hyphenation\{[\w\s\-]+\}//;
2203 $conf->config_orbase( 'invoice_latexreturnaddress',
2209 my $countrydefault = $conf->config('countrydefault') || 'US';
2210 if ( $cust_main->country eq $countrydefault ) {
2211 $invoice_data{'country'} = '';
2213 $invoice_data{'country'} =
2214 encode_entities(code2country($cust_main->country));
2218 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2219 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2221 $invoice_data{'notes'} =
2222 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2224 $invoice_data{'notes'} =
2226 s/%%(.*)$/<!-- $1 -->/g;
2227 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2228 s/\\begin\{enumerate\}/<ol>/g;
2230 s/\\end\{enumerate\}/<\/ol>/g;
2231 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2238 $conf->config_orbase('invoice_latexnotes', $template)
2242 # #do variable substitutions in notes
2243 # $invoice_data{'notes'} =
2245 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2246 # $conf->config_orbase('invoice_latexnotes', $suffix)
2250 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2251 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2253 $invoice_data{'footer'} =
2254 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2256 $invoice_data{'footer'} =
2257 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2258 $conf->config_orbase('invoice_latexfooter', $template)
2262 $invoice_data{'po_line'} =
2263 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2264 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2267 my $money_char = $conf->config('money_char') || '$';
2269 foreach my $line_item ( $self->_items($conf->exists('disable_previous_balance') ? qw( _items_pkg ) : () ) ) {
2271 ext_description => [],
2273 $detail->{'ref'} = $line_item->{'pkgnum'};
2274 $detail->{'description'} = encode_entities($line_item->{'description'});
2275 if ( exists $line_item->{'ext_description'} ) {
2276 @{$detail->{'ext_description'}} = map {
2277 encode_entities($_);
2278 } @{$line_item->{'ext_description'}};
2280 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2281 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2283 push @{$invoice_data{'detail_items'}}, $detail;
2288 foreach my $tax ( $self->_items_tax ) {
2290 $total->{'total_item'} = encode_entities($tax->{'description'});
2291 $taxtotal += $tax->{'amount'};
2292 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2293 push @{$invoice_data{'total_items'}}, $total;
2298 $total->{'total_item'} = 'Sub-total';
2299 $total->{'total_amount'} =
2300 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2301 unshift @{$invoice_data{'total_items'}}, $total;
2304 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2307 $total->{'total_item'} = '<b>Total</b>';
2308 $total->{'total_amount'} =
2311 $self->charged + ( $conf->exists('disable_previous_balance')
2317 push @{$invoice_data{'total_items'}}, $total;
2320 unless ($conf->exists('disable_previous_balance')) {
2321 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2324 foreach my $credit ( $self->_items_credits ) {
2326 $total->{'total_item'} = encode_entities($credit->{'description'});
2328 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2329 push @{$invoice_data{'total_items'}}, $total;
2333 foreach my $payment ( $self->_items_payments ) {
2335 $total->{'total_item'} = encode_entities($payment->{'description'});
2337 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2338 push @{$invoice_data{'total_items'}}, $total;
2343 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2344 $total->{'total_amount'} =
2345 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2346 push @{$invoice_data{'total_items'}}, $total;
2350 $html_template->fill_in( HASH => \%invoice_data);
2353 # quick subroutine for print_latex
2355 # There are ten characters that LaTeX treats as special characters, which
2356 # means that they do not simply typeset themselves:
2357 # # $ % & ~ _ ^ \ { }
2359 # TeX ignores blanks following an escaped character; if you want a blank (as
2360 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2364 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2365 $value =~ s/([<>])/\$$1\$/g;
2369 #utility methods for print_*
2371 sub balance_due_msg {
2373 my $msg = 'Balance Due';
2374 return $msg unless $conf->exists('invoice_default_terms');
2375 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2376 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2377 } elsif ( $conf->config('invoice_default_terms') ) {
2378 $msg .= ' - '. $conf->config('invoice_default_terms');
2385 my @display = scalar(@_)
2387 : qw( _items_previous _items_pkg );
2388 #: qw( _items_pkg );
2389 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2391 foreach my $display ( @display ) {
2392 push @b, $self->$display(@_);
2397 sub _items_previous {
2399 my $cust_main = $self->cust_main;
2400 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2402 foreach ( @pr_cust_bill ) {
2404 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2405 ' ('. time2str('%x',$_->_date). ')',
2406 #'pkgpart' => 'N/A',
2408 'amount' => sprintf("%.2f", $_->owed),
2414 # 'description' => 'Previous Balance',
2415 # #'pkgpart' => 'N/A',
2416 # 'pkgnum' => 'N/A',
2417 # 'amount' => sprintf("%10.2f", $pr_total ),
2418 # 'ext_description' => [ map {
2419 # "Invoice ". $_->invnum.
2420 # " (". time2str("%x",$_->_date). ") ".
2421 # sprintf("%10.2f", $_->owed)
2422 # } @pr_cust_bill ],
2429 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2430 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2435 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2436 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2439 sub _items_cust_bill_pkg {
2441 my $cust_bill_pkg = shift;
2444 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2446 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2448 my $desc = $cust_bill_pkg->desc;
2450 if ( $cust_bill_pkg->pkgnum > 0 ) {
2452 if ( $cust_bill_pkg->setup != 0 ) {
2453 my $description = $desc;
2454 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2455 my @d = $cust_pkg->h_labels_short($self->_date);
2456 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2458 description => $description,
2459 #pkgpart => $part_pkg->pkgpart,
2460 pkgnum => $cust_bill_pkg->pkgnum,
2461 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2462 ext_description => \@d,
2466 if ( $cust_bill_pkg->recur != 0 ) {
2468 description => $desc .
2469 ( $conf->exists('disable_line_item_date_ranges')
2471 : " (" .time2str("%x", $cust_bill_pkg->sdate).
2472 " - ".time2str("%x", $cust_bill_pkg->edate).")"
2474 #pkgpart => $part_pkg->pkgpart,
2475 pkgnum => $cust_bill_pkg->pkgnum,
2476 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2478 #at least until cust_bill_pkg has "past" ranges in addition to
2479 #the "future" sdate/edate ones... see #3032
2480 [ $cust_pkg->h_labels_short( $self->_date ),
2481 #$cust_bill_pkg->edate,
2482 #$cust_bill_pkg->sdate),
2483 $cust_bill_pkg->details,
2488 } else { #pkgnum tax or one-shot line item (??)
2490 if ( $cust_bill_pkg->setup != 0 ) {
2492 'description' => $desc,
2493 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2496 if ( $cust_bill_pkg->recur != 0 ) {
2498 'description' => "$desc (".
2499 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2500 time2str("%x", $cust_bill_pkg->edate). ')',
2501 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2513 sub _items_credits {
2518 foreach ( $self->cust_credited ) {
2520 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2522 my $reason = $_->cust_credit->reason;
2523 #my $reason = substr($_->cust_credit->reason,0,32);
2524 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2525 $reason = " ($reason) " if $reason;
2527 #'description' => 'Credit ref\#'. $_->crednum.
2528 # " (". time2str("%x",$_->cust_credit->_date) .")".
2530 'description' => 'Credit applied '.
2531 time2str("%x",$_->cust_credit->_date). $reason,
2532 'amount' => sprintf("%.2f",$_->amount),
2535 #foreach ( @cr_cust_credit ) {
2537 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2538 # $money_char. sprintf("%10.2f",$_->credited)
2546 sub _items_payments {
2550 #get & print payments
2551 foreach ( $self->cust_bill_pay ) {
2553 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2556 'description' => "Payment received ".
2557 time2str("%x",$_->cust_pay->_date ),
2558 'amount' => sprintf("%.2f", $_->amount )
2577 sub process_reprint {
2578 process_re_X('print', @_);
2585 sub process_reemail {
2586 process_re_X('email', @_);
2594 process_re_X('fax', @_);
2597 use Storable qw(thaw);
2601 my( $method, $job ) = ( shift, shift );
2602 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2604 my $param = thaw(decode_base64(shift));
2605 warn Dumper($param) if $DEBUG;
2616 my($method, $job, %param ) = @_;
2618 warn "re_X $method for job $job with param:\n".
2619 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2622 #some false laziness w/search/cust_bill.html
2624 my $orderby = 'ORDER BY cust_bill._date';
2626 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2628 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2630 my @cust_bill = qsearch( {
2631 #'select' => "cust_bill.*",
2632 'table' => 'cust_bill',
2633 'addl_from' => $addl_from,
2635 'extra_sql' => $extra_sql,
2636 'order_by' => $orderby,
2640 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2643 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2644 foreach my $cust_bill ( @cust_bill ) {
2645 $cust_bill->$method();
2647 if ( $job ) { #progressbar foo
2649 if ( time - $min_sec > $last ) {
2650 my $error = $job->update_statustext(
2651 int( 100 * $num / scalar(@cust_bill) )
2653 die $error if $error;
2664 =head1 CLASS METHODS
2670 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2676 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2681 Returns an SQL fragment to retreive the net amount (charged minus credited).
2687 'charged - '. $class->credited_sql;
2692 Returns an SQL fragment to retreive the amount paid against this invoice.
2698 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2699 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2704 Returns an SQL fragment to retreive the amount credited against this invoice.
2710 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2711 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2714 =item search_sql HASHREF
2716 Class method which returns an SQL WHERE fragment to search for parameters
2717 specified in HASHREF. Valid parameters are
2723 Epoch date (UNIX timestamp) setting a lower bound for _date values
2727 Epoch date (UNIX timestamp) setting an upper bound for _date values
2741 =item newest_percust
2745 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2750 my($class, $param) = @_;
2752 warn "$me search_sql called with params: \n".
2753 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2758 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2759 push @search, "cust_bill._date >= $1";
2761 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2762 push @search, "cust_bill._date < $1";
2764 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2765 push @search, "cust_bill.invnum >= $1";
2767 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2768 push @search, "cust_bill.invnum <= $1";
2770 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2771 push @search, "cust_main.agentnum = $1";
2774 push @search, '0 != '. FS::cust_bill->owed_sql
2775 if $param->{'open'};
2777 push @search, '0 != '. FS::cust_bill->net_sql
2780 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2781 if $param->{'days'};
2783 if ( $param->{'newest_percust'} ) {
2785 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2786 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2788 my @newest_where = map { my $x = $_;
2789 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2792 grep ! /^cust_main./, @search;
2793 my $newest_where = scalar(@newest_where)
2794 ? ' AND '. join(' AND ', @newest_where)
2798 push @search, "cust_bill._date = (
2799 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2800 WHERE newest_cust_bill.custnum = cust_bill.custnum
2806 my $curuser = $FS::CurrentUser::CurrentUser;
2807 if ( $curuser->username eq 'fs_queue'
2808 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2810 my $newuser = qsearchs('access_user', {
2811 'username' => $username,
2815 $curuser = $newuser;
2817 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2821 push @search, $curuser->agentnums_sql;
2823 join(' AND ', @search );
2835 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2836 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base