4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
28 use FS::cust_bill_pay;
29 use FS::cust_bill_pay_batch;
30 use FS::part_bill_event;
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
36 $me = '[FS::cust_bill]';
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub {
41 $money_char = $conf->config('money_char') || '$';
46 FS::cust_bill - Object methods for cust_bill records
52 $record = new FS::cust_bill \%hash;
53 $record = new FS::cust_bill { 'column' => 'value' };
55 $error = $record->insert;
57 $error = $new_record->replace($old_record);
59 $error = $record->delete;
61 $error = $record->check;
63 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
65 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
67 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
69 @cust_pay_objects = $cust_bill->cust_pay;
71 $tax_amount = $record->tax;
73 @lines = $cust_bill->print_text;
74 @lines = $cust_bill->print_text $time;
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
81 following fields are currently supported:
85 =item invnum - primary key (assigned automatically for new invoices)
87 =item custnum - customer (see L<FS::cust_main>)
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
92 =item charged - amount of this invoice
94 =item printed - deprecated
96 =item closed - books closed flag, empty or `Y'
106 Creates a new invoice. To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
112 sub table { 'cust_bill'; }
114 sub cust_linked { $_[0]->cust_main_custnum; }
115 sub cust_unlinked_msg {
117 "WARNING: can't find cust_main.custnum ". $self->custnum.
118 ' (cust_bill.invnum '. $self->invnum. ')';
123 Adds this invoice to the database ("Posts" the invoice). If there is an error,
124 returns the error, otherwise returns false.
128 This method now works but you probably shouldn't use it. Instead, apply a
129 credit against the invoice.
131 Using this method to delete invoices outright is really, really bad. There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
135 Really, don't use it.
141 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
142 $self->SUPER::delete(@_);
145 =item replace OLD_RECORD
147 Replaces the OLD_RECORD with this one in the database. If there is an error,
148 returns the error, otherwise returns false.
150 Only printed may be changed. printed is normally updated by calling the
151 collect method of a customer object (see L<FS::cust_main>).
155 #replace can be inherited from Record.pm
157 # replace_check is now the preferred way to #implement replace data checks
158 # (so $object->replace() works without an argument)
161 my( $new, $old ) = ( shift, shift );
162 return "Can't change custnum!" unless $old->custnum == $new->custnum;
163 #return "Can't change _date!" unless $old->_date eq $new->_date;
164 return "Can't change _date!" unless $old->_date == $new->_date;
165 return "Can't change charged!" unless $old->charged == $new->charged
166 || $old->charged == 0;
173 Checks all fields to make sure this is a valid invoice. If there is an error,
174 returns the error, otherwise returns false. Called by the insert and replace
183 $self->ut_numbern('invnum')
184 || $self->ut_number('custnum')
185 || $self->ut_numbern('_date')
186 || $self->ut_money('charged')
187 || $self->ut_numbern('printed')
188 || $self->ut_enum('closed', [ '', 'Y' ])
190 return $error if $error;
192 return "Unknown customer"
193 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
195 $self->_date(time) unless $self->_date;
197 $self->printed(0) if $self->printed eq '';
204 Returns a list consisting of the total previous balance for this customer,
205 followed by the previous outstanding invoices (as FS::cust_bill objects also).
212 my @cust_bill = sort { $a->_date <=> $b->_date }
213 grep { $_->owed != 0 && $_->_date < $self->_date }
214 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
216 foreach ( @cust_bill ) { $total += $_->owed; }
222 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
228 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
233 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
240 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
242 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
245 =item open_cust_bill_pkg
247 Returns the open line items for this invoice.
249 Note that cust_bill_pkg with both setup and recur fees are returned as two
250 separate line items, each with only one fee.
254 # modeled after cust_main::open_cust_bill
255 sub open_cust_bill_pkg {
258 # grep { $_->owed > 0 } $self->cust_bill_pkg
260 my %other = ( 'recur' => 'setup',
261 'setup' => 'recur', );
263 foreach my $field ( qw( recur setup )) {
264 push @open, map { $_->set( $other{$field}, 0 ); $_; }
265 grep { $_->owed($field) > 0 }
266 $self->cust_bill_pkg;
272 =item cust_bill_event
274 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
279 sub cust_bill_event {
281 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
287 Returns the customer (see L<FS::cust_main>) for this invoice.
293 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
296 =item cust_suspend_if_balance_over AMOUNT
298 Suspends the customer associated with this invoice if the total amount owed on
299 this invoice and all older invoices is greater than the specified amount.
301 Returns a list: an empty list on success or a list of errors.
305 sub cust_suspend_if_balance_over {
306 my( $self, $amount ) = ( shift, shift );
307 my $cust_main = $self->cust_main;
308 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
311 $cust_main->suspend(@_);
317 Depreciated. See the cust_credited method.
319 #Returns a list consisting of the total previous credited (see
320 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
321 #outstanding credits (FS::cust_credit objects).
327 croak "FS::cust_bill->cust_credit depreciated; see ".
328 "FS::cust_bill->cust_credit_bill";
331 #my @cust_credit = sort { $a->_date <=> $b->_date }
332 # grep { $_->credited != 0 && $_->_date < $self->_date }
333 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
335 #foreach (@cust_credit) { $total += $_->credited; }
336 #$total, @cust_credit;
341 Depreciated. See the cust_bill_pay method.
343 #Returns all payments (see L<FS::cust_pay>) for this invoice.
349 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
351 #sort { $a->_date <=> $b->_date }
352 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
358 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
364 sort { $a->_date <=> $b->_date }
365 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
370 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
376 sort { $a->_date <=> $b->_date }
377 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
383 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
390 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
392 foreach (@taxlines) { $total += $_->setup; }
398 Returns the amount owed (still outstanding) on this invoice, which is charged
399 minus all payment applications (see L<FS::cust_bill_pay>) and credit
400 applications (see L<FS::cust_credit_bill>).
406 my $balance = $self->charged;
407 $balance -= $_->amount foreach ( $self->cust_bill_pay );
408 $balance -= $_->amount foreach ( $self->cust_credited );
409 $balance = sprintf( "%.2f", $balance);
410 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
414 =item apply_payments_and_credits
418 sub apply_payments_and_credits {
421 local $SIG{HUP} = 'IGNORE';
422 local $SIG{INT} = 'IGNORE';
423 local $SIG{QUIT} = 'IGNORE';
424 local $SIG{TERM} = 'IGNORE';
425 local $SIG{TSTP} = 'IGNORE';
426 local $SIG{PIPE} = 'IGNORE';
428 my $oldAutoCommit = $FS::UID::AutoCommit;
429 local $FS::UID::AutoCommit = 0;
432 $self->select_for_update; #mutex
434 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
435 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
437 while ( $self->owed > 0 and ( @payments || @credits ) ) {
440 if ( @payments && @credits ) {
442 #decide which goes first by weight of top (unapplied) line item
444 my @open_lineitems = $self->open_cust_bill_pkg;
447 max( map { $_->part_pkg->pay_weight || 0 }
452 my $max_credit_weight =
453 max( map { $_->part_pkg->credit_weight || 0 }
459 #if both are the same... payments first? it has to be something
460 if ( $max_pay_weight >= $max_credit_weight ) {
466 } elsif ( @payments ) {
468 } elsif ( @credits ) {
471 die "guru meditation #12 and 35";
474 if ( $app eq 'pay' ) {
476 my $payment = shift @payments;
478 $app = new FS::cust_bill_pay {
479 'paynum' => $payment->paynum,
480 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
483 } elsif ( $app eq 'credit' ) {
485 my $credit = shift @credits;
487 $app = new FS::cust_credit_bill {
488 'crednum' => $credit->crednum,
489 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
493 die "guru meditation #12 and 35";
496 $app->invnum( $self->invnum );
498 my $error = $app->insert;
500 $dbh->rollback if $oldAutoCommit;
501 return "Error inserting ". $app->table. " record: $error";
503 die $error if $error;
507 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
512 =item generate_email PARAMHASH
514 PARAMHASH can contain the following:
518 =item from => sender address, required
520 =item tempate => alternate template name, optional
522 =item print_text => text attachment arrayref, optional
524 =item subject => email subject, optional
528 Returns an argument list to be passed to L<FS::Misc::send_email>.
539 my $me = '[FS::cust_bill::generate_email]';
542 'from' => $args{'from'},
543 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
546 if (ref($args{'to'}) eq 'ARRAY') {
547 $return{'to'} = $args{'to'};
549 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
550 $self->cust_main->invoicing_list
554 if ( $conf->exists('invoice_html') ) {
556 warn "$me creating HTML/text multipart message"
559 $return{'nobody'} = 1;
561 my $alternative = build MIME::Entity
562 'Type' => 'multipart/alternative',
563 'Encoding' => '7bit',
564 'Disposition' => 'inline'
568 if ( $conf->exists('invoice_email_pdf')
569 and scalar($conf->config('invoice_email_pdf_note')) ) {
571 warn "$me using 'invoice_email_pdf_note' in multipart message"
573 $data = [ map { $_ . "\n" }
574 $conf->config('invoice_email_pdf_note')
579 warn "$me not using 'invoice_email_pdf_note' in multipart message"
581 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
582 $data = $args{'print_text'};
584 $data = [ $self->print_text('', $args{'template'}) ];
589 $alternative->attach(
590 'Type' => 'text/plain',
591 #'Encoding' => 'quoted-printable',
592 'Encoding' => '7bit',
594 'Disposition' => 'inline',
597 $args{'from'} =~ /\@([\w\.\-]+)/;
598 my $from = $1 || 'example.com';
599 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
601 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
603 if ( defined($args{'template'}) && length($args{'template'})
604 && -e "$path/logo_". $args{'template'}. ".png"
607 $file = "$path/logo_". $args{'template'}. ".png";
609 $file = "$path/logo.png";
612 my $image = build MIME::Entity
613 'Type' => 'image/png',
614 'Encoding' => 'base64',
616 'Filename' => 'logo.png',
617 'Content-ID' => "<$content_id>",
620 $alternative->attach(
621 'Type' => 'text/html',
622 'Encoding' => 'quoted-printable',
623 'Data' => [ '<html>',
626 ' '. encode_entities($return{'subject'}),
629 ' <body bgcolor="#e8e8e8">',
630 $self->print_html('', $args{'template'}, $content_id),
634 'Disposition' => 'inline',
635 #'Filename' => 'invoice.pdf',
638 if ( $conf->exists('invoice_email_pdf') ) {
643 # multipart/alternative
649 my $related = build MIME::Entity 'Type' => 'multipart/related',
650 'Encoding' => '7bit';
652 #false laziness w/Misc::send_email
653 $related->head->replace('Content-type',
655 '; boundary="'. $related->head->multipart_boundary. '"'.
656 '; type=multipart/alternative'
659 $related->add_part($alternative);
661 $related->add_part($image);
663 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
665 $return{'mimeparts'} = [ $related, $pdf ];
669 #no other attachment:
671 # multipart/alternative
676 $return{'content-type'} = 'multipart/related';
677 $return{'mimeparts'} = [ $alternative, $image ];
678 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
679 #$return{'disposition'} = 'inline';
685 if ( $conf->exists('invoice_email_pdf') ) {
686 warn "$me creating PDF attachment"
689 #mime parts arguments a la MIME::Entity->build().
690 $return{'mimeparts'} = [
691 { $self->mimebuild_pdf('', $args{'template'}) }
695 if ( $conf->exists('invoice_email_pdf')
696 and scalar($conf->config('invoice_email_pdf_note')) ) {
698 warn "$me using 'invoice_email_pdf_note'"
700 $return{'body'} = [ map { $_ . "\n" }
701 $conf->config('invoice_email_pdf_note')
706 warn "$me not using 'invoice_email_pdf_note'"
708 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
709 $return{'body'} = $args{'print_text'};
711 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
724 Returns a list suitable for passing to MIME::Entity->build(), representing
725 this invoice as PDF attachment.
732 'Type' => 'application/pdf',
733 'Encoding' => 'base64',
734 'Data' => [ $self->print_pdf(@_) ],
735 'Disposition' => 'attachment',
736 'Filename' => 'invoice.pdf',
740 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
742 Sends this invoice to the destinations configured for this customer: sends
743 email, prints and/or faxes. See L<FS::cust_main_invoice>.
745 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
747 AGENTNUM, if specified, means that this invoice will only be sent for customers
748 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
749 single agent) or an arrayref of agentnums.
751 INVOICE_FROM, if specified, overrides the default email invoice From: address.
753 AMOUNT, if specified, only sends the invoice if the total amount owed on this
754 invoice and all older invoices is greater than the specified amount.
761 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
762 or die "invalid invoice number: " . $opt{invnum};
764 my @args = ( $opt{template}, $opt{agentnum} );
765 push @args, $opt{invoice_from}
766 if exists($opt{invoice_from}) && $opt{invoice_from};
768 my $error = $self->send( @args );
769 die $error if $error;
775 my $template = scalar(@_) ? shift : '';
776 if ( scalar(@_) && $_[0] ) {
777 my $agentnums = ref($_[0]) ? shift : [ shift ];
778 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
784 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
786 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
789 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
791 my @invoicing_list = $self->cust_main->invoicing_list;
793 #$self->email_invoice($template, $invoice_from)
794 $self->email($template, $invoice_from)
795 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
797 #$self->print_invoice($template)
798 $self->print($template)
799 if grep { $_ eq 'POST' } @invoicing_list; #postal
801 $self->fax_invoice($template)
802 if grep { $_ eq 'FAX' } @invoicing_list; #fax
808 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
812 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
814 INVOICE_FROM, if specified, overrides the default email invoice From: address.
818 sub queueable_email {
821 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
822 or die "invalid invoice number: " . $opt{invnum};
824 my @args = ( $opt{template} );
825 push @args, $opt{invoice_from}
826 if exists($opt{invoice_from}) && $opt{invoice_from};
828 my $error = $self->email( @args );
829 die $error if $error;
836 my $template = scalar(@_) ? shift : '';
840 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
842 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
843 $self->cust_main->invoicing_list;
845 #better to notify this person than silence
846 @invoicing_list = ($invoice_from) unless @invoicing_list;
848 my $error = send_email(
849 $self->generate_email(
850 'from' => $invoice_from,
851 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
852 'template' => $template,
855 die "can't email invoice: $error\n" if $error;
856 #die "$error\n" if $error;
860 =item lpr_data [ TEMPLATENAME ]
862 Returns the postscript or plaintext for this invoice as an arrayref.
864 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
869 my( $self, $template) = @_;
870 $conf->exists('invoice_latex')
871 ? [ $self->print_ps('', $template) ]
872 : [ $self->print_text('', $template) ];
875 =item print [ TEMPLATENAME ]
879 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
886 my $template = scalar(@_) ? shift : '';
888 do_print $self->lpr_data($template);
891 =item fax_invoice [ TEMPLATENAME ]
895 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
901 my $template = scalar(@_) ? shift : '';
903 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
904 unless $conf->exists('invoice_latex');
906 my $dialstring = $self->cust_main->getfield('fax');
909 my $error = send_fax( 'docdata' => $self->lpr_data($template),
910 'dialstring' => $dialstring,
912 die $error if $error;
916 =item ftp_invoice [ TEMPLATENAME ]
918 Sends this invoice data via FTP.
920 TEMPLATENAME is unused?
926 my $template = scalar(@_) ? shift : '';
930 'server' => $conf->config('cust_bill-ftpserver'),
931 'username' => $conf->config('cust_bill-ftpusername'),
932 'password' => $conf->config('cust_bill-ftppassword'),
933 'dir' => $conf->config('cust_bill-ftpdir'),
934 'format' => $conf->config('cust_bill-ftpformat'),
938 =item spool_invoice [ TEMPLATENAME ]
940 Spools this invoice data (see L<FS::spool_csv>)
942 TEMPLATENAME is unused?
948 my $template = scalar(@_) ? shift : '';
951 'format' => $conf->config('cust_bill-spoolformat'),
952 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
956 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
958 Like B<send>, but only sends the invoice if it is the newest open invoice for
968 grep { $_->owed > 0 }
969 qsearch('cust_bill', {
970 'custnum' => $self->custnum,
971 #'_date' => { op=>'>', value=>$self->_date },
972 'invnum' => { op=>'>', value=>$self->invnum },
979 =item send_csv OPTION => VALUE, ...
981 Sends invoice as a CSV data-file to a remote host with the specified protocol.
985 protocol - currently only "ftp"
991 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
992 and YYMMDDHHMMSS is a timestamp.
994 See L</print_csv> for a description of the output format.
999 my($self, %opt) = @_;
1003 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1004 mkdir $spooldir, 0700 unless -d $spooldir;
1006 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1007 my $file = "$spooldir/$tracctnum.csv";
1009 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1011 open(CSV, ">$file") or die "can't open $file: $!";
1019 if ( $opt{protocol} eq 'ftp' ) {
1020 eval "use Net::FTP;";
1022 $net = Net::FTP->new($opt{server}) or die @$;
1024 die "unknown protocol: $opt{protocol}";
1027 $net->login( $opt{username}, $opt{password} )
1028 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1030 $net->binary or die "can't set binary mode";
1032 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1034 $net->put($file) or die "can't put $file: $!";
1044 Spools CSV invoice data.
1050 =item format - 'default' or 'billco'
1052 =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>).
1054 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1056 =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.
1063 my($self, %opt) = @_;
1065 my $cust_main = $self->cust_main;
1067 if ( $opt{'dest'} ) {
1068 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1069 $cust_main->invoicing_list;
1070 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1071 || ! keys %invoicing_list;
1074 if ( $opt{'balanceover'} ) {
1076 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1079 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1080 mkdir $spooldir, 0700 unless -d $spooldir;
1082 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1086 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1087 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1090 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1092 open(CSV, ">>$file") or die "can't open $file: $!";
1093 flock(CSV, LOCK_EX);
1098 if ( lc($opt{'format'}) eq 'billco' ) {
1100 flock(CSV, LOCK_UN);
1105 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1108 open(CSV,">>$file") or die "can't open $file: $!";
1109 flock(CSV, LOCK_EX);
1115 flock(CSV, LOCK_UN);
1122 =item print_csv OPTION => VALUE, ...
1124 Returns CSV data for this invoice.
1128 format - 'default' or 'billco'
1130 Returns a list consisting of two scalars. The first is a single line of CSV
1131 header information for this invoice. The second is one or more lines of CSV
1132 detail information for this invoice.
1134 If I<format> is not specified or "default", the fields of the CSV file are as
1137 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1141 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1143 B<record_type> is C<cust_bill> for the initial header line only. The
1144 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1145 fields are filled in.
1147 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1148 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1151 =item invnum - invoice number
1153 =item custnum - customer number
1155 =item _date - invoice date
1157 =item charged - total invoice amount
1159 =item first - customer first name
1161 =item last - customer first name
1163 =item company - company name
1165 =item address1 - address line 1
1167 =item address2 - address line 1
1177 =item pkg - line item description
1179 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1181 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1183 =item sdate - start date for recurring fee
1185 =item edate - end date for recurring fee
1189 If I<format> is "billco", the fields of the header CSV file are as follows:
1191 +-------------------------------------------------------------------+
1192 | FORMAT HEADER FILE |
1193 |-------------------------------------------------------------------|
1194 | Field | Description | Name | Type | Width |
1195 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1196 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1197 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1198 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1199 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1200 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1201 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1202 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1203 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1204 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1205 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1206 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1207 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1208 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1209 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1210 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1211 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1212 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1213 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1214 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1215 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1216 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1217 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1218 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1219 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1220 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1221 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1222 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1223 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1224 +-------+-------------------------------+------------+------+-------+
1226 If I<format> is "billco", the fields of the detail CSV file are as follows:
1228 FORMAT FOR DETAIL FILE
1230 Field | Description | Name | Type | Width
1231 1 | N/A-Leave Empty | RC | CHAR | 2
1232 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1233 3 | Account Number | TRACCTNUM | CHAR | 15
1234 4 | Invoice Number | TRINVOICE | CHAR | 15
1235 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1236 6 | Transaction Detail | DETAILS | CHAR | 100
1237 7 | Amount | AMT | NUM* | 9
1238 8 | Line Format Control** | LNCTRL | CHAR | 2
1239 9 | Grouping Code | GROUP | CHAR | 2
1240 10 | User Defined | ACCT CODE | CHAR | 15
1245 my($self, %opt) = @_;
1247 eval "use Text::CSV_XS";
1250 my $cust_main = $self->cust_main;
1252 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1254 if ( lc($opt{'format'}) eq 'billco' ) {
1257 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1259 my $duedate = $self->balance_due_date;
1261 my( $previous_balance, @unused ) = $self->previous; #previous balance
1263 my $pmt_cr_applied = 0;
1264 $pmt_cr_applied += $_->{'amount'}
1265 foreach ( $self->_items_payments, $self->_items_credits ) ;
1267 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1270 '', # 1 | N/A-Leave Empty CHAR 2
1271 '', # 2 | N/A-Leave Empty CHAR 15
1272 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1273 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1274 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1275 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1276 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1277 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1278 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1279 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1280 '', # 10 | Ancillary Billing Information CHAR 30
1281 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1282 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1285 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1288 $duedate, # 14 | Bill Due Date CHAR 10
1290 $previous_balance, # 15 | Previous Balance NUM* 9
1291 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1292 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1293 $totaldue, # 18 | Total Amt Due NUM* 9
1294 $totaldue, # 19 | Total Amt Due NUM* 9
1295 '', # 20 | 30 Day Aging NUM* 9
1296 '', # 21 | 60 Day Aging NUM* 9
1297 '', # 22 | 90 Day Aging NUM* 9
1298 'N', # 23 | Y/N CHAR 1
1299 '', # 24 | Remittance automation CHAR 100
1300 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1301 $self->custnum, # 26 | Customer Reference Number CHAR 15
1302 '0', # 27 | Federal Tax*** NUM* 9
1303 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1304 '0', # 29 | Other Taxes & Fees*** NUM* 9
1313 time2str("%x", $self->_date),
1314 sprintf("%.2f", $self->charged),
1315 ( map { $cust_main->getfield($_) }
1316 qw( first last company address1 address2 city state zip country ) ),
1318 ) or die "can't create csv";
1321 my $header = $csv->string. "\n";
1324 if ( lc($opt{'format'}) eq 'billco' ) {
1327 foreach my $item ( $self->_items_pkg ) {
1330 '', # 1 | N/A-Leave Empty CHAR 2
1331 '', # 2 | N/A-Leave Empty CHAR 15
1332 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1333 $self->invnum, # 4 | Invoice Number CHAR 15
1334 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1335 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1336 $item->{'amount'}, # 7 | Amount NUM* 9
1337 '', # 8 | Line Format Control** CHAR 2
1338 '', # 9 | Grouping Code CHAR 2
1339 '', # 10 | User Defined CHAR 15
1342 $detail .= $csv->string. "\n";
1348 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1350 my($pkg, $setup, $recur, $sdate, $edate);
1351 if ( $cust_bill_pkg->pkgnum ) {
1353 ($pkg, $setup, $recur, $sdate, $edate) = (
1354 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1355 ( $cust_bill_pkg->setup != 0
1356 ? sprintf("%.2f", $cust_bill_pkg->setup )
1358 ( $cust_bill_pkg->recur != 0
1359 ? sprintf("%.2f", $cust_bill_pkg->recur )
1361 ( $cust_bill_pkg->sdate
1362 ? time2str("%x", $cust_bill_pkg->sdate)
1364 ($cust_bill_pkg->edate
1365 ?time2str("%x", $cust_bill_pkg->edate)
1369 } else { #pkgnum tax
1370 next unless $cust_bill_pkg->setup != 0;
1371 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1372 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1374 ($pkg, $setup, $recur, $sdate, $edate) =
1375 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1381 ( map { '' } (1..11) ),
1382 ($pkg, $setup, $recur, $sdate, $edate)
1383 ) or die "can't create csv";
1385 $detail .= $csv->string. "\n";
1391 ( $header, $detail );
1397 Pays this invoice with a compliemntary payment. If there is an error,
1398 returns the error, otherwise returns false.
1404 my $cust_pay = new FS::cust_pay ( {
1405 'invnum' => $self->invnum,
1406 'paid' => $self->owed,
1409 'payinfo' => $self->cust_main->payinfo,
1417 Attempts to pay this invoice with a credit card payment via a
1418 Business::OnlinePayment realtime gateway. See
1419 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1420 for supported processors.
1426 $self->realtime_bop( 'CC', @_ );
1431 Attempts to pay this invoice with an electronic check (ACH) payment via a
1432 Business::OnlinePayment realtime gateway. See
1433 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1434 for supported processors.
1440 $self->realtime_bop( 'ECHECK', @_ );
1445 Attempts to pay this invoice with phone bill (LEC) payment via a
1446 Business::OnlinePayment realtime gateway. See
1447 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1448 for supported processors.
1454 $self->realtime_bop( 'LEC', @_ );
1458 my( $self, $method ) = @_;
1460 my $cust_main = $self->cust_main;
1461 my $balance = $cust_main->balance;
1462 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1463 $amount = sprintf("%.2f", $amount);
1464 return "not run (balance $balance)" unless $amount > 0;
1466 my $description = 'Internet Services';
1467 if ( $conf->exists('business-onlinepayment-description') ) {
1468 my $dtempl = $conf->config('business-onlinepayment-description');
1470 my $agent_obj = $cust_main->agent
1471 or die "can't retreive agent for $cust_main (agentnum ".
1472 $cust_main->agentnum. ")";
1473 my $agent = $agent_obj->agent;
1474 my $pkgs = join(', ',
1475 map { $_->cust_pkg->part_pkg->pkg }
1476 grep { $_->pkgnum } $self->cust_bill_pkg
1478 $description = eval qq("$dtempl");
1481 $cust_main->realtime_bop($method, $amount,
1482 'description' => $description,
1483 'invnum' => $self->invnum,
1488 =item batch_card OPTION => VALUE...
1490 Adds a payment for this invoice to the pending credit card batch (see
1491 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1492 runs the payment using a realtime gateway.
1497 my ($self, %options) = @_;
1498 my $cust_main = $self->cust_main;
1500 $options{invnum} = $self->invnum;
1502 $cust_main->batch_card(%options);
1505 sub _agent_template {
1507 $self->cust_main->agent_template;
1510 sub _agent_invoice_from {
1512 $self->cust_main->agent_invoice_from;
1515 =item print_text [ TIME [ , TEMPLATE ] ]
1517 Returns an text invoice, as a list of lines.
1519 TIME an optional value used to control the printing of overdue messages. The
1520 default is now. It isn't the date of the invoice; that's the `_date' field.
1521 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1522 L<Time::Local> and L<Date::Parse> for conversion functions.
1526 #still some false laziness w/_items stuff (and send_csv)
1529 my( $self, $today, $template ) = @_;
1532 # my $invnum = $self->invnum;
1533 my $cust_main = $self->cust_main;
1534 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1535 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1537 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1538 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1539 #my $balance_due = $self->owed + $pr_total - $cr_total;
1540 my $balance_due = $self->owed + $pr_total;
1543 #my($description,$amount);
1547 unless ($conf->exists('disable_previous_balance')) {
1548 foreach ( @pr_cust_bill ) {
1550 "Previous Balance, Invoice #". $_->invnum.
1551 " (". time2str("%x",$_->_date). ")",
1552 $money_char. sprintf("%10.2f",$_->owed)
1555 if (@pr_cust_bill) {
1556 push @buf,['','-----------'];
1557 push @buf,[ 'Total Previous Balance',
1558 $money_char. sprintf("%10.2f",$pr_total ) ];
1564 foreach my $cust_bill_pkg (
1565 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1566 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1569 my $desc = $cust_bill_pkg->desc;
1571 if ( $cust_bill_pkg->pkgnum > 0 ) {
1573 if ( $cust_bill_pkg->setup != 0 ) {
1574 my $description = $desc;
1575 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1576 push @buf, [ $description,
1577 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1579 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1580 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1583 if ( $cust_bill_pkg->recur != 0 ) {
1586 ( $conf->exists('disable_line_item_date_ranges')
1588 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1589 time2str("%x", $cust_bill_pkg->edate) . ")"
1591 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1594 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1595 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1596 $cust_bill_pkg->sdate );
1599 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1601 } else { #pkgnum tax or one-shot line item
1603 if ( $cust_bill_pkg->setup != 0 ) {
1605 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1607 if ( $cust_bill_pkg->recur != 0 ) {
1608 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1609 . time2str("%x", $cust_bill_pkg->edate). ")",
1610 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1618 push @buf,['','-----------'];
1619 push @buf,[ ( $conf->exists('disable_previous_balance')
1621 : 'Total New Charges'),
1622 $money_char. sprintf("%10.2f",$self->charged) ];
1625 unless ($conf->exists('disable_previous_balance')) {
1626 push @buf,['','-----------'];
1627 push @buf,['Total Charges',
1628 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1632 foreach ( $self->cust_credited ) {
1634 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1636 my $reason = substr($_->cust_credit->reason,0,32);
1637 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1638 $reason = " ($reason) " if $reason;
1640 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1642 $money_char. sprintf("%10.2f",$_->amount)
1645 #foreach ( @cr_cust_credit ) {
1647 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1648 # $money_char. sprintf("%10.2f",$_->credited)
1652 #get & print payments
1653 foreach ( $self->cust_bill_pay ) {
1655 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1658 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1659 $money_char. sprintf("%10.2f",$_->amount )
1664 my $balance_due_msg = $self->balance_due_msg;
1666 push @buf,['','-----------'];
1667 push @buf,[$balance_due_msg, $money_char.
1668 sprintf("%10.2f", $balance_due ) ];
1671 #create the template
1672 $template ||= $self->_agent_template;
1673 my $templatefile = 'invoice_template';
1674 $templatefile .= "_$template" if length($template);
1675 my @invoice_template = $conf->config($templatefile)
1676 or die "cannot load config file $templatefile";
1679 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1680 /invoice_lines\((\d*)\)/;
1681 $invoice_lines += $1 || scalar(@buf);
1684 die "no invoice_lines() functions in template?" unless $wasfunc;
1685 my $invoice_template = new Text::Template (
1687 SOURCE => [ map "$_\n", @invoice_template ],
1688 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1689 $invoice_template->compile()
1690 or die "can't compile template: $Text::Template::ERROR";
1692 #setup template variables
1693 package FS::cust_bill::_template; #!
1694 use vars qw( $custnum $invnum $date $agent @address $overdue
1695 $page $total_pages @buf );
1697 $custnum = $self->custnum;
1698 $invnum = $self->invnum;
1699 $date = $self->_date;
1700 $agent = $self->cust_main->agent->agent;
1703 if ( $FS::cust_bill::invoice_lines ) {
1705 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1707 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1712 #format address (variable for the template)
1714 @address = ( '', '', '', '', '', '' );
1715 package FS::cust_bill; #!
1716 $FS::cust_bill::_template::address[$l++] =
1717 $cust_main->payname.
1718 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1719 ? " (P.O. #". $cust_main->payinfo. ")"
1723 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1724 if $cust_main->company;
1725 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1726 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1727 if $cust_main->address2;
1728 $FS::cust_bill::_template::address[$l++] =
1729 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1731 my $countrydefault = $conf->config('countrydefault') || 'US';
1732 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1733 unless $cust_main->country eq $countrydefault;
1735 # #overdue? (variable for the template)
1736 # $FS::cust_bill::_template::overdue = (
1738 # && $today > $self->_date
1739 ## && $self->printed > 1
1740 # && $self->printed > 0
1743 #and subroutine for the template
1744 sub FS::cust_bill::_template::invoice_lines {
1745 my $lines = shift || scalar(@buf);
1747 scalar(@buf) ? shift @buf : [ '', '' ];
1753 $FS::cust_bill::_template::page = 1;
1757 push @collect, split("\n",
1758 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1760 $FS::cust_bill::_template::page++;
1763 map "$_\n", @collect;
1767 =item print_latex [ TIME [ , TEMPLATE ] ]
1769 Internal method - returns a filename of a filled-in LaTeX template for this
1770 invoice (Note: add ".tex" to get the actual filename).
1772 See print_ps and print_pdf for methods that return PostScript and PDF output.
1774 TIME an optional value used to control the printing of overdue messages. The
1775 default is now. It isn't the date of the invoice; that's the `_date' field.
1776 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1777 L<Time::Local> and L<Date::Parse> for conversion functions.
1781 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1784 my( $self, $today, $template ) = @_;
1786 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1789 my $cust_main = $self->cust_main;
1790 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1791 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1793 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1794 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1795 #my $balance_due = $self->owed + $pr_total - $cr_total;
1796 my $balance_due = $self->owed + $pr_total;
1798 #create the template
1799 $template ||= $self->_agent_template;
1800 my $templatefile = 'invoice_latex';
1801 my $suffix = length($template) ? "_$template" : '';
1802 $templatefile .= $suffix;
1803 my @invoice_template = map "$_\n", $conf->config($templatefile)
1804 or die "cannot load config file $templatefile";
1806 my($format, $text_template);
1807 if ( grep { /^%%Detail/ } @invoice_template ) {
1808 #change this to a die when the old code is removed
1809 warn "old-style invoice template $templatefile; ".
1810 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1813 $format = 'Text::Template';
1814 $text_template = new Text::Template(
1816 SOURCE => \@invoice_template,
1817 DELIMITERS => [ '[@--', '--@]' ],
1820 $text_template->compile()
1821 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1825 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1826 $returnaddress = join("\n",
1827 $conf->config_orbase('invoice_latexreturnaddress', $template)
1830 $returnaddress = '~';
1833 my %invoice_data = (
1834 'custnum' => $self->custnum,
1835 'invnum' => $self->invnum,
1836 'date' => time2str('%b %o, %Y', $self->_date),
1837 'today' => time2str('%b %o, %Y', $today),
1838 'agent' => _latex_escape($cust_main->agent->agent),
1839 'agent_custid' => _latex_escape($cust_main->agent_custid),
1840 'payname' => _latex_escape($cust_main->payname),
1841 'company' => _latex_escape($cust_main->company),
1842 'address1' => _latex_escape($cust_main->address1),
1843 'address2' => _latex_escape($cust_main->address2),
1844 'city' => _latex_escape($cust_main->city),
1845 'state' => _latex_escape($cust_main->state),
1847 'zip' => _latex_escape($cust_main->zip),
1848 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1849 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1850 'returnaddress' => $returnaddress,
1852 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1853 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1854 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1855 'current_charges' => sprintf('%.2f', $self->charged ),
1856 'previous_balance' => sprintf("%.2f", $pr_total),
1857 'balance' => sprintf("%.2f", $balance_due),
1858 'duedate' => $self->balance_due_date,
1859 'ship_enable' => $conf->exists('invoice-ship_address'),
1860 'unitprices' => $conf->exists('invoice-unitprice'),
1863 my $countrydefault = $conf->config('countrydefault') || 'US';
1864 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1865 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1866 my $method = $prefix.$_;
1867 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1869 $invoice_data{'ship_country'} = ''
1870 if ( $invoice_data{'ship_country'} eq $countrydefault );
1872 if ( $cust_main->country eq $countrydefault ) {
1873 $invoice_data{'country'} = '';
1875 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1878 $invoice_data{'notes'} =
1880 # #do variable substitutions in notes
1881 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1882 $conf->config_orbase('invoice_latexnotes', $template)
1884 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1887 #do variable substitution in coupon
1888 foreach my $include (qw( coupon )) {
1890 my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1892 my $inc_tt = new Text::Template (
1894 SOURCE => [ map "$_\n", @inc_src ],
1895 DELIMITERS => [ '[@--', '--@]' ],
1896 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1898 unless ( $inc_tt->compile() ) {
1899 my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1900 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1904 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1906 $invoice_data{$include} =~ s/\n+$//
1909 $invoice_data{'footer'} =~ s/\n+$//;
1910 $invoice_data{'smallfooter'} =~ s/\n+$//;
1911 $invoice_data{'notes'} =~ s/\n+$//;
1913 $invoice_data{'po_line'} =
1914 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1915 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1919 if ( $format eq 'old' ) {
1922 my @total_item = ();
1923 while ( @invoice_template ) {
1924 my $line = shift @invoice_template;
1926 if ( $line =~ /^%%Detail\s*$/ ) {
1928 while ( ( my $line_item_line = shift @invoice_template )
1929 !~ /^%%EndDetail\s*$/ ) {
1930 push @line_item, $line_item_line;
1932 foreach my $line_item ( $self->_items ) {
1933 #foreach my $line_item ( $self->_items_pkg ) {
1934 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1935 $invoice_data{'description'} =
1936 _latex_escape($line_item->{'description'});
1937 if ( exists $line_item->{'ext_description'} ) {
1938 $invoice_data{'description'} .=
1939 "\\tabularnewline\n~~".
1940 join( "\\tabularnewline\n~~",
1941 map _latex_escape($_), @{$line_item->{'ext_description'}}
1944 $invoice_data{'amount'} = $line_item->{'amount'};
1945 $invoice_data{'unit_amount'} = $line_item->{'unit_amount'};
1946 $invoice_data{'quantity'} = $line_item->{'quantity'};
1947 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1949 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1952 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1954 while ( ( my $total_item_line = shift @invoice_template )
1955 !~ /^%%EndTotalDetails\s*$/ ) {
1956 push @total_item, $total_item_line;
1959 my @total_fill = ();
1962 foreach my $tax ( $self->_items_tax ) {
1963 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1964 $taxtotal += $tax->{'amount'};
1965 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1967 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1972 $invoice_data{'total_item'} = 'Sub-total';
1973 $invoice_data{'total_amount'} =
1974 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1975 unshift @total_fill,
1976 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1980 $invoice_data{'total_item'} = '\textbf{Total}';
1981 $invoice_data{'total_amount'} =
1982 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1984 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1987 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1990 foreach my $credit ( $self->_items_credits ) {
1991 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1993 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1995 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2000 foreach my $payment ( $self->_items_payments ) {
2001 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2003 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2005 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2009 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2010 $invoice_data{'total_amount'} =
2011 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2013 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2016 push @filled_in, @total_fill;
2019 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2020 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2021 push @filled_in, $line;
2032 } elsif ( $format eq 'Text::Template' ) {
2034 my @detail_items = ();
2035 my @total_items = ();
2037 $invoice_data{'detail_items'} = \@detail_items;
2038 $invoice_data{'total_items'} = \@total_items;
2040 my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2041 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2043 ext_description => [],
2045 $detail->{'ref'} = $line_item->{'pkgnum'};
2046 $detail->{'quantity'} = 1;
2047 $detail->{'description'} = _latex_escape($line_item->{'description'});
2048 if ( exists $line_item->{'ext_description'} ) {
2049 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2051 $detail->{'amount'} = $line_item->{'amount'};
2052 $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2053 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2055 push @detail_items, $detail;
2060 foreach my $tax ( $self->_items_tax ) {
2062 $total->{'total_item'} = _latex_escape($tax->{'description'});
2063 $taxtotal += $tax->{'amount'};
2064 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2065 push @total_items, $total;
2069 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2071 $total->{'total_item'} = 'Sub-total';
2072 $total->{'total_amount'} =
2073 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2074 unshift @total_items, $total;
2076 $invoice_data{'taxtotal'} = '0.00';
2081 $total->{'total_item'} = '\textbf{Total}';
2082 $total->{'total_amount'} =
2085 $self->charged + ( $conf->exists('disable_previous_balance')
2091 push @total_items, $total;
2094 unless ($conf->exists('disable_previous_balance')) {
2095 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2098 my $credittotal = 0;
2099 foreach my $credit ( $self->_items_credits ) {
2101 $total->{'total_item'} = _latex_escape($credit->{'description'});
2102 $credittotal += $credit->{'amount'};
2103 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2104 push @total_items, $total;
2106 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2109 my $paymenttotal = 0;
2110 foreach my $payment ( $self->_items_payments ) {
2112 $total->{'total_item'} = _latex_escape($payment->{'description'});
2113 $paymenttotal += $payment->{'amount'};
2114 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2115 push @total_items, $total;
2117 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2121 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2122 $total->{'total_amount'} =
2123 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2124 push @total_items, $total;
2129 die "guru meditation #54";
2132 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2133 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2137 ) or die "can't open temp file: $!\n";
2138 if ( $format eq 'old' ) {
2139 print $fh join('', @filled_in );
2140 } elsif ( $format eq 'Text::Template' ) {
2141 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2143 die "guru meditation #32";
2147 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2152 =item print_ps [ TIME [ , TEMPLATE ] ]
2154 Returns an postscript invoice, as a scalar.
2156 TIME an optional value used to control the printing of overdue messages. The
2157 default is now. It isn't the date of the invoice; that's the `_date' field.
2158 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2159 L<Time::Local> and L<Date::Parse> for conversion functions.
2166 my $file = $self->print_latex(@_);
2167 my $ps = generate_ps($file);
2172 =item print_pdf [ TIME [ , TEMPLATE ] ]
2174 Returns an PDF invoice, as a scalar.
2176 TIME an optional value used to control the printing of overdue messages. The
2177 default is now. It isn't the date of the invoice; that's the `_date' field.
2178 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2179 L<Time::Local> and L<Date::Parse> for conversion functions.
2186 my $file = $self->print_latex(@_);
2187 my $pdf = generate_pdf($file);
2192 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2194 Returns an HTML invoice, as a scalar.
2196 TIME an optional value used to control the printing of overdue messages. The
2197 default is now. It isn't the date of the invoice; that's the `_date' field.
2198 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2199 L<Time::Local> and L<Date::Parse> for conversion functions.
2201 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2202 when emailing the invoice as part of a multipart/related MIME email.
2206 #some falze laziness w/print_text and print_latex (and send_csv)
2208 my( $self, $today, $template, $cid ) = @_;
2211 my $cust_main = $self->cust_main;
2212 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2213 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2215 $template ||= $self->_agent_template;
2216 my $templatefile = 'invoice_html';
2217 my $suffix = length($template) ? "_$template" : '';
2218 $templatefile .= $suffix;
2219 my @html_template = map "$_\n", $conf->config($templatefile)
2220 or die "cannot load config file $templatefile";
2222 my $html_template = new Text::Template(
2224 SOURCE => \@html_template,
2225 DELIMITERS => [ '<%=', '%>' ],
2228 $html_template->compile()
2229 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2231 my %invoice_data = (
2232 'custnum' => $self->custnum,
2233 'invnum' => $self->invnum,
2234 'date' => time2str('%b %o, %Y', $self->_date),
2235 'today' => time2str('%b %o, %Y', $today),
2236 'agent' => encode_entities($cust_main->agent->agent),
2237 'agent_custid' => encode_entities($cust_main->agent_custid),
2238 'payname' => encode_entities($cust_main->payname),
2239 'company' => encode_entities($cust_main->company),
2240 'address1' => encode_entities($cust_main->address1),
2241 'address2' => encode_entities($cust_main->address2),
2242 'city' => encode_entities($cust_main->city),
2243 'state' => encode_entities($cust_main->state),
2244 'zip' => encode_entities($cust_main->zip),
2245 'terms' => $conf->config('invoice_default_terms')
2246 || 'Payable upon receipt',
2248 'template' => $template,
2249 'ship_enable' => $conf->exists('invoice-ship_address'),
2250 'unitprices' => $conf->exists('invoice-unitprice'),
2251 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2254 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2255 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2256 my $method = $prefix.$_;
2257 $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2261 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2262 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2264 $invoice_data{'returnaddress'} =
2265 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2267 $invoice_data{'returnaddress'} =
2270 s/\\\\\*?\s*$/<BR>/;
2271 s/\\hyphenation\{[\w\s\-]+\}//;
2274 $conf->config_orbase( 'invoice_latexreturnaddress',
2280 my $countrydefault = $conf->config('countrydefault') || 'US';
2281 if ( $cust_main->country eq $countrydefault ) {
2282 $invoice_data{'country'} = '';
2284 $invoice_data{'country'} =
2285 encode_entities(code2country($cust_main->country));
2289 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2290 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2292 $invoice_data{'notes'} =
2293 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2295 $invoice_data{'notes'} =
2297 s/%%(.*)$/<!-- $1 -->/g;
2298 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2299 s/\\begin\{enumerate\}/<ol>/g;
2301 s/\\end\{enumerate\}/<\/ol>/g;
2302 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2309 $conf->config_orbase('invoice_latexnotes', $template)
2313 # #do variable substitutions in notes
2314 # $invoice_data{'notes'} =
2316 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2317 # $conf->config_orbase('invoice_latexnotes', $suffix)
2321 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2322 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2324 $invoice_data{'footer'} =
2325 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2327 $invoice_data{'footer'} =
2328 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2329 $conf->config_orbase('invoice_latexfooter', $template)
2333 $invoice_data{'po_line'} =
2334 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2335 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2338 my $money_char = $conf->config('money_char') || '$';
2340 my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2341 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2343 ext_description => [],
2345 $detail->{'ref'} = $line_item->{'pkgnum'};
2346 $detail->{'description'} = encode_entities($line_item->{'description'});
2347 if ( exists $line_item->{'ext_description'} ) {
2348 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2350 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2351 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2353 push @{$invoice_data{'detail_items'}}, $detail;
2358 foreach my $tax ( $self->_items_tax ) {
2360 $total->{'total_item'} = encode_entities($tax->{'description'});
2361 $taxtotal += $tax->{'amount'};
2362 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2363 push @{$invoice_data{'total_items'}}, $total;
2368 $total->{'total_item'} = 'Sub-total';
2369 $total->{'total_amount'} =
2370 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2371 unshift @{$invoice_data{'total_items'}}, $total;
2374 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2377 $total->{'total_item'} = '<b>Total</b>';
2378 $total->{'total_amount'} =
2381 $self->charged + ( $conf->exists('disable_previous_balance')
2387 push @{$invoice_data{'total_items'}}, $total;
2390 unless ($conf->exists('disable_previous_balance')) {
2391 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2394 foreach my $credit ( $self->_items_credits ) {
2396 $total->{'total_item'} = encode_entities($credit->{'description'});
2398 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2399 push @{$invoice_data{'total_items'}}, $total;
2403 foreach my $payment ( $self->_items_payments ) {
2405 $total->{'total_item'} = encode_entities($payment->{'description'});
2407 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2408 push @{$invoice_data{'total_items'}}, $total;
2413 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2414 $total->{'total_amount'} =
2415 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2416 push @{$invoice_data{'total_items'}}, $total;
2420 $html_template->fill_in( HASH => \%invoice_data);
2423 # quick subroutine for print_latex
2425 # There are ten characters that LaTeX treats as special characters, which
2426 # means that they do not simply typeset themselves:
2427 # # $ % & ~ _ ^ \ { }
2429 # TeX ignores blanks following an escaped character; if you want a blank (as
2430 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2434 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2435 $value =~ s/([<>])/\$$1\$/g;
2439 #utility methods for print_*
2441 sub balance_due_msg {
2443 my $msg = 'Balance Due';
2444 return $msg unless $conf->exists('invoice_default_terms');
2445 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2446 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2447 } elsif ( $conf->config('invoice_default_terms') ) {
2448 $msg .= ' - '. $conf->config('invoice_default_terms');
2453 sub balance_due_date {
2456 if ( $conf->exists('invoice_default_terms')
2457 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2458 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2463 =item invnum_date_pretty
2465 Returns a string with the invoice number and date, for example:
2466 "Invoice #54 (3/20/2008)"
2470 sub invnum_date_pretty {
2472 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2478 #my @display = scalar(@_)
2480 # : qw( _items_previous _items_pkg );
2481 # #: qw( _items_pkg );
2482 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2483 my @display = qw( _items_previous _items_pkg );
2486 foreach my $display ( @display ) {
2487 push @b, $self->$display(@_);
2492 sub _items_previous {
2494 my $cust_main = $self->cust_main;
2495 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2497 foreach ( @pr_cust_bill ) {
2499 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2500 ' ('. time2str('%x',$_->_date). ')',
2501 #'pkgpart' => 'N/A',
2503 'amount' => sprintf("%.2f", $_->owed),
2509 # 'description' => 'Previous Balance',
2510 # #'pkgpart' => 'N/A',
2511 # 'pkgnum' => 'N/A',
2512 # 'amount' => sprintf("%10.2f", $pr_total ),
2513 # 'ext_description' => [ map {
2514 # "Invoice ". $_->invnum.
2515 # " (". time2str("%x",$_->_date). ") ".
2516 # sprintf("%10.2f", $_->owed)
2517 # } @pr_cust_bill ],
2524 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2525 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2530 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2531 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2534 sub _items_cust_bill_pkg {
2536 my $cust_bill_pkg = shift;
2539 my $format = $opt{format} || '';
2540 my $escape_function = $opt{escape_function} || sub { shift };
2543 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2545 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2547 my $desc = $cust_bill_pkg->desc;
2549 my %details_opt = ( 'format' => $format,
2550 'escape_function' => $escape_function,
2553 if ( $cust_bill_pkg->pkgnum > 0 ) {
2555 if ( $cust_bill_pkg->setup != 0 ) {
2557 my $description = $desc;
2558 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2560 my @d = map &{$escape_function}($_),
2561 $cust_pkg->h_labels_short($self->_date);
2562 push @d, $cust_bill_pkg->details(%details_opt)
2563 if $cust_bill_pkg->recur == 0;
2566 description => $description,
2567 #pkgpart => $part_pkg->pkgpart,
2568 pkgnum => $cust_bill_pkg->pkgnum,
2569 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2570 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2571 quantity => $cust_bill_pkg->quantity,
2572 ext_description => \@d,
2576 if ( $cust_bill_pkg->recur != 0 ) {
2578 my $description = $desc;
2579 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2580 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2581 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2584 #at least until cust_bill_pkg has "past" ranges in addition to
2585 #the "future" sdate/edate ones... see #3032
2586 my @d = map &{$escape_function}($_),
2587 $cust_pkg->h_labels_short($self->_date);
2588 #$cust_bill_pkg->edate,
2589 #$cust_bill_pkg->sdate),
2590 push @d, $cust_bill_pkg->details(%details_opt);
2593 description => $description,
2594 #pkgpart => $part_pkg->pkgpart,
2595 pkgnum => $cust_bill_pkg->pkgnum,
2596 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2597 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2598 quantity => $cust_bill_pkg->quantity,
2599 ext_description => \@d,
2604 } else { #pkgnum tax or one-shot line item (??)
2606 if ( $cust_bill_pkg->setup != 0 ) {
2608 'description' => $desc,
2609 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2612 if ( $cust_bill_pkg->recur != 0 ) {
2614 'description' => "$desc (".
2615 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2616 time2str("%x", $cust_bill_pkg->edate). ')',
2617 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2629 sub _items_credits {
2634 foreach ( $self->cust_credited ) {
2636 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2638 my $reason = $_->cust_credit->reason;
2639 #my $reason = substr($_->cust_credit->reason,0,32);
2640 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2641 $reason = " ($reason) " if $reason;
2643 #'description' => 'Credit ref\#'. $_->crednum.
2644 # " (". time2str("%x",$_->cust_credit->_date) .")".
2646 'description' => 'Credit applied '.
2647 time2str("%x",$_->cust_credit->_date). $reason,
2648 'amount' => sprintf("%.2f",$_->amount),
2651 #foreach ( @cr_cust_credit ) {
2653 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2654 # $money_char. sprintf("%10.2f",$_->credited)
2662 sub _items_payments {
2666 #get & print payments
2667 foreach ( $self->cust_bill_pay ) {
2669 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2672 'description' => "Payment received ".
2673 time2str("%x",$_->cust_pay->_date ),
2674 'amount' => sprintf("%.2f", $_->amount )
2689 =item process_reprint
2693 sub process_reprint {
2694 process_re_X('print', @_);
2697 =item process_reemail
2701 sub process_reemail {
2702 process_re_X('email', @_);
2710 process_re_X('fax', @_);
2718 process_re_X('ftp', @_);
2725 sub process_respool {
2726 process_re_X('spool', @_);
2729 use Storable qw(thaw);
2733 my( $method, $job ) = ( shift, shift );
2734 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2736 my $param = thaw(decode_base64(shift));
2737 warn Dumper($param) if $DEBUG;
2748 my($method, $job, %param ) = @_;
2750 warn "re_X $method for job $job with param:\n".
2751 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2754 #some false laziness w/search/cust_bill.html
2756 my $orderby = 'ORDER BY cust_bill._date';
2758 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2760 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2762 my @cust_bill = qsearch( {
2763 #'select' => "cust_bill.*",
2764 'table' => 'cust_bill',
2765 'addl_from' => $addl_from,
2767 'extra_sql' => $extra_sql,
2768 'order_by' => $orderby,
2772 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2774 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2777 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2778 foreach my $cust_bill ( @cust_bill ) {
2779 $cust_bill->$method();
2781 if ( $job ) { #progressbar foo
2783 if ( time - $min_sec > $last ) {
2784 my $error = $job->update_statustext(
2785 int( 100 * $num / scalar(@cust_bill) )
2787 die $error if $error;
2798 =head1 CLASS METHODS
2804 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2810 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2815 Returns an SQL fragment to retreive the net amount (charged minus credited).
2821 'charged - '. $class->credited_sql;
2826 Returns an SQL fragment to retreive the amount paid against this invoice.
2832 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2833 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2838 Returns an SQL fragment to retreive the amount credited against this invoice.
2844 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2845 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2848 =item search_sql HASHREF
2850 Class method which returns an SQL WHERE fragment to search for parameters
2851 specified in HASHREF. Valid parameters are
2857 Epoch date (UNIX timestamp) setting a lower bound for _date values
2861 Epoch date (UNIX timestamp) setting an upper bound for _date values
2875 =item newest_percust
2879 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2884 my($class, $param) = @_;
2886 warn "$me search_sql called with params: \n".
2887 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2892 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2893 push @search, "cust_bill._date >= $1";
2895 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2896 push @search, "cust_bill._date < $1";
2898 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2899 push @search, "cust_bill.invnum >= $1";
2901 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2902 push @search, "cust_bill.invnum <= $1";
2904 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2905 push @search, "cust_main.agentnum = $1";
2908 push @search, '0 != '. FS::cust_bill->owed_sql
2909 if $param->{'open'};
2911 push @search, '0 != '. FS::cust_bill->net_sql
2914 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2915 if $param->{'days'};
2917 if ( $param->{'newest_percust'} ) {
2919 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2920 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2922 my @newest_where = map { my $x = $_;
2923 $x =~ s/\bcust_bill\./newest_cust_bill./g;
2926 grep ! /^cust_main./, @search;
2927 my $newest_where = scalar(@newest_where)
2928 ? ' AND '. join(' AND ', @newest_where)
2932 push @search, "cust_bill._date = (
2933 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
2934 WHERE newest_cust_bill.custnum = cust_bill.custnum
2940 my $curuser = $FS::CurrentUser::CurrentUser;
2941 if ( $curuser->username eq 'fs_queue'
2942 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
2944 my $newuser = qsearchs('access_user', {
2945 'username' => $username,
2949 $curuser = $newuser;
2951 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
2955 push @search, $curuser->agentnums_sql;
2957 join(' AND ', @search );
2969 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2970 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base