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;
29 use FS::cust_bill_pay;
30 use FS::cust_bill_pay_batch;
31 use FS::part_bill_event;
34 @ISA = qw( FS::cust_main_Mixin FS::Record );
37 $me = '[FS::cust_bill]';
39 #ask FS::UID to run this stuff for us later
40 FS::UID->install_callback( sub {
42 $money_char = $conf->config('money_char') || '$';
47 FS::cust_bill - Object methods for cust_bill records
53 $record = new FS::cust_bill \%hash;
54 $record = new FS::cust_bill { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
66 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
68 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
70 @cust_pay_objects = $cust_bill->cust_pay;
72 $tax_amount = $record->tax;
74 @lines = $cust_bill->print_text;
75 @lines = $cust_bill->print_text $time;
79 An FS::cust_bill object represents an invoice; a declaration that a customer
80 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
81 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
82 following fields are currently supported:
86 =item invnum - primary key (assigned automatically for new invoices)
88 =item custnum - customer (see L<FS::cust_main>)
90 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
91 L<Time::Local> and L<Date::Parse> for conversion functions.
93 =item charged - amount of this invoice
95 =item printed - deprecated
97 =item closed - books closed flag, empty or `Y'
107 Creates a new invoice. To add the invoice to the database, see L<"insert">.
108 Invoices are normally created by calling the bill method of a customer object
109 (see L<FS::cust_main>).
113 sub table { 'cust_bill'; }
115 sub cust_linked { $_[0]->cust_main_custnum; }
116 sub cust_unlinked_msg {
118 "WARNING: can't find cust_main.custnum ". $self->custnum.
119 ' (cust_bill.invnum '. $self->invnum. ')';
124 Adds this invoice to the database ("Posts" the invoice). If there is an error,
125 returns the error, otherwise returns false.
129 This method now works but you probably shouldn't use it. Instead, apply a
130 credit against the invoice.
132 Using this method to delete invoices outright is really, really bad. There
133 would be no record you ever posted this invoice, and there are no check to
134 make sure charged = 0 or that there are no associated cust_bill_pkg records.
136 Really, don't use it.
142 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
143 $self->SUPER::delete(@_);
146 =item replace OLD_RECORD
148 Replaces the OLD_RECORD with this one in the database. If there is an error,
149 returns the error, otherwise returns false.
151 Only printed may be changed. printed is normally updated by calling the
152 collect method of a customer object (see L<FS::cust_main>).
156 #replace can be inherited from Record.pm
158 # replace_check is now the preferred way to #implement replace data checks
159 # (so $object->replace() works without an argument)
162 my( $new, $old ) = ( shift, shift );
163 return "Can't change custnum!" unless $old->custnum == $new->custnum;
164 #return "Can't change _date!" unless $old->_date eq $new->_date;
165 return "Can't change _date!" unless $old->_date == $new->_date;
166 return "Can't change charged!" unless $old->charged == $new->charged
167 || $old->charged == 0;
174 Checks all fields to make sure this is a valid invoice. If there is an error,
175 returns the error, otherwise returns false. Called by the insert and replace
184 $self->ut_numbern('invnum')
185 || $self->ut_number('custnum')
186 || $self->ut_numbern('_date')
187 || $self->ut_money('charged')
188 || $self->ut_numbern('printed')
189 || $self->ut_enum('closed', [ '', 'Y' ])
191 return $error if $error;
193 return "Unknown customer"
194 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
196 $self->_date(time) unless $self->_date;
198 $self->printed(0) if $self->printed eq '';
205 Returns a list consisting of the total previous balance for this customer,
206 followed by the previous outstanding invoices (as FS::cust_bill objects also).
213 my @cust_bill = sort { $a->_date <=> $b->_date }
214 grep { $_->owed != 0 && $_->_date < $self->_date }
215 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
217 foreach ( @cust_bill ) { $total += $_->owed; }
223 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
229 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
234 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
241 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
243 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
246 =item open_cust_bill_pkg
248 Returns the open line items for this invoice.
250 Note that cust_bill_pkg with both setup and recur fees are returned as two
251 separate line items, each with only one fee.
255 # modeled after cust_main::open_cust_bill
256 sub open_cust_bill_pkg {
259 # grep { $_->owed > 0 } $self->cust_bill_pkg
261 my %other = ( 'recur' => 'setup',
262 'setup' => 'recur', );
264 foreach my $field ( qw( recur setup )) {
265 push @open, map { $_->set( $other{$field}, 0 ); $_; }
266 grep { $_->owed($field) > 0 }
267 $self->cust_bill_pkg;
273 =item cust_bill_event
275 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
279 sub cust_bill_event {
281 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
284 =item num_cust_bill_event
286 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
290 sub num_cust_bill_event {
293 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
294 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
295 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
296 $sth->fetchrow_arrayref->[0];
301 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
305 #false laziness w/cust_pkg.pm
309 'table' => 'cust_event',
310 'addl_from' => 'JOIN part_event USING ( eventpart )',
311 'hashref' => { 'tablenum' => $self->invnum },
312 'extra_sql' => " AND eventtable = 'cust_bill' ",
318 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
322 #false laziness w/cust_pkg.pm
326 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
327 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
328 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
329 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
330 $sth->fetchrow_arrayref->[0];
335 Returns the customer (see L<FS::cust_main>) for this invoice.
341 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
344 =item cust_suspend_if_balance_over AMOUNT
346 Suspends the customer associated with this invoice if the total amount owed on
347 this invoice and all older invoices is greater than the specified amount.
349 Returns a list: an empty list on success or a list of errors.
353 sub cust_suspend_if_balance_over {
354 my( $self, $amount ) = ( shift, shift );
355 my $cust_main = $self->cust_main;
356 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
359 $cust_main->suspend(@_);
365 Depreciated. See the cust_credited method.
367 #Returns a list consisting of the total previous credited (see
368 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
369 #outstanding credits (FS::cust_credit objects).
375 croak "FS::cust_bill->cust_credit depreciated; see ".
376 "FS::cust_bill->cust_credit_bill";
379 #my @cust_credit = sort { $a->_date <=> $b->_date }
380 # grep { $_->credited != 0 && $_->_date < $self->_date }
381 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
383 #foreach (@cust_credit) { $total += $_->credited; }
384 #$total, @cust_credit;
389 Depreciated. See the cust_bill_pay method.
391 #Returns all payments (see L<FS::cust_pay>) for this invoice.
397 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
399 #sort { $a->_date <=> $b->_date }
400 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
406 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
412 sort { $a->_date <=> $b->_date }
413 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
418 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
424 sort { $a->_date <=> $b->_date }
425 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
431 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
438 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
440 foreach (@taxlines) { $total += $_->setup; }
446 Returns the amount owed (still outstanding) on this invoice, which is charged
447 minus all payment applications (see L<FS::cust_bill_pay>) and credit
448 applications (see L<FS::cust_credit_bill>).
454 my $balance = $self->charged;
455 $balance -= $_->amount foreach ( $self->cust_bill_pay );
456 $balance -= $_->amount foreach ( $self->cust_credited );
457 $balance = sprintf( "%.2f", $balance);
458 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
462 =item apply_payments_and_credits
466 sub apply_payments_and_credits {
469 local $SIG{HUP} = 'IGNORE';
470 local $SIG{INT} = 'IGNORE';
471 local $SIG{QUIT} = 'IGNORE';
472 local $SIG{TERM} = 'IGNORE';
473 local $SIG{TSTP} = 'IGNORE';
474 local $SIG{PIPE} = 'IGNORE';
476 my $oldAutoCommit = $FS::UID::AutoCommit;
477 local $FS::UID::AutoCommit = 0;
480 $self->select_for_update; #mutex
482 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
483 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
485 while ( $self->owed > 0 and ( @payments || @credits ) ) {
488 if ( @payments && @credits ) {
490 #decide which goes first by weight of top (unapplied) line item
492 my @open_lineitems = $self->open_cust_bill_pkg;
495 max( map { $_->part_pkg->pay_weight || 0 }
500 my $max_credit_weight =
501 max( map { $_->part_pkg->credit_weight || 0 }
507 #if both are the same... payments first? it has to be something
508 if ( $max_pay_weight >= $max_credit_weight ) {
514 } elsif ( @payments ) {
516 } elsif ( @credits ) {
519 die "guru meditation #12 and 35";
522 if ( $app eq 'pay' ) {
524 my $payment = shift @payments;
526 $app = new FS::cust_bill_pay {
527 'paynum' => $payment->paynum,
528 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
531 } elsif ( $app eq 'credit' ) {
533 my $credit = shift @credits;
535 $app = new FS::cust_credit_bill {
536 'crednum' => $credit->crednum,
537 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
541 die "guru meditation #12 and 35";
544 $app->invnum( $self->invnum );
546 my $error = $app->insert;
548 $dbh->rollback if $oldAutoCommit;
549 return "Error inserting ". $app->table. " record: $error";
551 die $error if $error;
555 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
560 =item generate_email OPTION => VALUE ...
568 sender address, required
572 alternate template name, optional
576 text attachment arrayref, optional
580 email subject, optional
584 Returns an argument list to be passed to L<FS::Misc::send_email>.
595 my $me = '[FS::cust_bill::generate_email]';
598 'from' => $args{'from'},
599 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
602 if (ref($args{'to'}) eq 'ARRAY') {
603 $return{'to'} = $args{'to'};
605 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
606 $self->cust_main->invoicing_list
610 if ( $conf->exists('invoice_html') ) {
612 warn "$me creating HTML/text multipart message"
615 $return{'nobody'} = 1;
617 my $alternative = build MIME::Entity
618 'Type' => 'multipart/alternative',
619 'Encoding' => '7bit',
620 'Disposition' => 'inline'
624 if ( $conf->exists('invoice_email_pdf')
625 and scalar($conf->config('invoice_email_pdf_note')) ) {
627 warn "$me using 'invoice_email_pdf_note' in multipart message"
629 $data = [ map { $_ . "\n" }
630 $conf->config('invoice_email_pdf_note')
635 warn "$me not using 'invoice_email_pdf_note' in multipart message"
637 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
638 $data = $args{'print_text'};
640 $data = [ $self->print_text('', $args{'template'}) ];
645 $alternative->attach(
646 'Type' => 'text/plain',
647 #'Encoding' => 'quoted-printable',
648 'Encoding' => '7bit',
650 'Disposition' => 'inline',
653 $args{'from'} =~ /\@([\w\.\-]+)/;
654 my $from = $1 || 'example.com';
655 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
657 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
659 if ( defined($args{'template'}) && length($args{'template'})
660 && -e "$path/logo_". $args{'template'}. ".png"
663 $file = "$path/logo_". $args{'template'}. ".png";
665 $file = "$path/logo.png";
668 my $image = build MIME::Entity
669 'Type' => 'image/png',
670 'Encoding' => 'base64',
672 'Filename' => 'logo.png',
673 'Content-ID' => "<$content_id>",
676 $alternative->attach(
677 'Type' => 'text/html',
678 'Encoding' => 'quoted-printable',
679 'Data' => [ '<html>',
682 ' '. encode_entities($return{'subject'}),
685 ' <body bgcolor="#e8e8e8">',
686 $self->print_html('', $args{'template'}, $content_id),
690 'Disposition' => 'inline',
691 #'Filename' => 'invoice.pdf',
694 if ( $conf->exists('invoice_email_pdf') ) {
699 # multipart/alternative
705 my $related = build MIME::Entity 'Type' => 'multipart/related',
706 'Encoding' => '7bit';
708 #false laziness w/Misc::send_email
709 $related->head->replace('Content-type',
711 '; boundary="'. $related->head->multipart_boundary. '"'.
712 '; type=multipart/alternative'
715 $related->add_part($alternative);
717 $related->add_part($image);
719 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
721 $return{'mimeparts'} = [ $related, $pdf ];
725 #no other attachment:
727 # multipart/alternative
732 $return{'content-type'} = 'multipart/related';
733 $return{'mimeparts'} = [ $alternative, $image ];
734 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
735 #$return{'disposition'} = 'inline';
741 if ( $conf->exists('invoice_email_pdf') ) {
742 warn "$me creating PDF attachment"
745 #mime parts arguments a la MIME::Entity->build().
746 $return{'mimeparts'} = [
747 { $self->mimebuild_pdf('', $args{'template'}) }
751 if ( $conf->exists('invoice_email_pdf')
752 and scalar($conf->config('invoice_email_pdf_note')) ) {
754 warn "$me using 'invoice_email_pdf_note'"
756 $return{'body'} = [ map { $_ . "\n" }
757 $conf->config('invoice_email_pdf_note')
762 warn "$me not using 'invoice_email_pdf_note'"
764 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
765 $return{'body'} = $args{'print_text'};
767 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
780 Returns a list suitable for passing to MIME::Entity->build(), representing
781 this invoice as PDF attachment.
788 'Type' => 'application/pdf',
789 'Encoding' => 'base64',
790 'Data' => [ $self->print_pdf(@_) ],
791 'Disposition' => 'attachment',
792 'Filename' => 'invoice.pdf',
796 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
798 Sends this invoice to the destinations configured for this customer: sends
799 email, prints and/or faxes. See L<FS::cust_main_invoice>.
801 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
803 AGENTNUM, if specified, means that this invoice will only be sent for customers
804 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
805 single agent) or an arrayref of agentnums.
807 INVOICE_FROM, if specified, overrides the default email invoice From: address.
809 AMOUNT, if specified, only sends the invoice if the total amount owed on this
810 invoice and all older invoices is greater than the specified amount.
817 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
818 or die "invalid invoice number: " . $opt{invnum};
820 my @args = ( $opt{template}, $opt{agentnum} );
821 push @args, $opt{invoice_from}
822 if exists($opt{invoice_from}) && $opt{invoice_from};
824 my $error = $self->send( @args );
825 die $error if $error;
831 my $template = scalar(@_) ? shift : '';
832 if ( scalar(@_) && $_[0] ) {
833 my $agentnums = ref($_[0]) ? shift : [ shift ];
834 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
840 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
842 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
845 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
847 my @invoicing_list = $self->cust_main->invoicing_list;
849 #$self->email_invoice($template, $invoice_from)
850 $self->email($template, $invoice_from)
851 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
853 #$self->print_invoice($template)
854 $self->print($template)
855 if grep { $_ eq 'POST' } @invoicing_list; #postal
857 $self->fax_invoice($template)
858 if grep { $_ eq 'FAX' } @invoicing_list; #fax
864 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
868 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
870 INVOICE_FROM, if specified, overrides the default email invoice From: address.
874 sub queueable_email {
877 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
878 or die "invalid invoice number: " . $opt{invnum};
880 my @args = ( $opt{template} );
881 push @args, $opt{invoice_from}
882 if exists($opt{invoice_from}) && $opt{invoice_from};
884 my $error = $self->email( @args );
885 die $error if $error;
892 my $template = scalar(@_) ? shift : '';
896 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
898 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
899 $self->cust_main->invoicing_list;
901 #better to notify this person than silence
902 @invoicing_list = ($invoice_from) unless @invoicing_list;
904 my $error = send_email(
905 $self->generate_email(
906 'from' => $invoice_from,
907 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
908 'template' => $template,
911 die "can't email invoice: $error\n" if $error;
912 #die "$error\n" if $error;
916 =item lpr_data [ TEMPLATENAME ]
918 Returns the postscript or plaintext for this invoice as an arrayref.
920 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
925 my( $self, $template) = @_;
926 $conf->exists('invoice_latex')
927 ? [ $self->print_ps('', $template) ]
928 : [ $self->print_text('', $template) ];
931 =item print [ TEMPLATENAME ]
935 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
942 my $template = scalar(@_) ? shift : '';
944 do_print $self->lpr_data($template);
947 =item fax_invoice [ TEMPLATENAME ]
951 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
957 my $template = scalar(@_) ? shift : '';
959 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
960 unless $conf->exists('invoice_latex');
962 my $dialstring = $self->cust_main->getfield('fax');
965 my $error = send_fax( 'docdata' => $self->lpr_data($template),
966 'dialstring' => $dialstring,
968 die $error if $error;
972 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
974 Like B<send>, but only sends the invoice if it is the newest open invoice for
984 grep { $_->owed > 0 }
985 qsearch('cust_bill', {
986 'custnum' => $self->custnum,
987 #'_date' => { op=>'>', value=>$self->_date },
988 'invnum' => { op=>'>', value=>$self->invnum },
995 =item send_csv OPTION => VALUE, ...
997 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1001 protocol - currently only "ftp"
1007 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1008 and YYMMDDHHMMSS is a timestamp.
1010 See L</print_csv> for a description of the output format.
1015 my($self, %opt) = @_;
1019 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1020 mkdir $spooldir, 0700 unless -d $spooldir;
1022 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1023 my $file = "$spooldir/$tracctnum.csv";
1025 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1027 open(CSV, ">$file") or die "can't open $file: $!";
1035 if ( $opt{protocol} eq 'ftp' ) {
1036 eval "use Net::FTP;";
1038 $net = Net::FTP->new($opt{server}) or die @$;
1040 die "unknown protocol: $opt{protocol}";
1043 $net->login( $opt{username}, $opt{password} )
1044 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1046 $net->binary or die "can't set binary mode";
1048 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1050 $net->put($file) or die "can't put $file: $!";
1060 Spools CSV invoice data.
1066 =item format - 'default' or 'billco'
1068 =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>).
1070 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1072 =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.
1079 my($self, %opt) = @_;
1081 my $cust_main = $self->cust_main;
1083 if ( $opt{'dest'} ) {
1084 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1085 $cust_main->invoicing_list;
1086 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1087 || ! keys %invoicing_list;
1090 if ( $opt{'balanceover'} ) {
1092 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1095 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1096 mkdir $spooldir, 0700 unless -d $spooldir;
1098 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1102 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1103 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1106 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1108 open(CSV, ">>$file") or die "can't open $file: $!";
1109 flock(CSV, LOCK_EX);
1114 if ( lc($opt{'format'}) eq 'billco' ) {
1116 flock(CSV, LOCK_UN);
1121 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1124 open(CSV,">>$file") or die "can't open $file: $!";
1125 flock(CSV, LOCK_EX);
1131 flock(CSV, LOCK_UN);
1138 =item print_csv OPTION => VALUE, ...
1140 Returns CSV data for this invoice.
1144 format - 'default' or 'billco'
1146 Returns a list consisting of two scalars. The first is a single line of CSV
1147 header information for this invoice. The second is one or more lines of CSV
1148 detail information for this invoice.
1150 If I<format> is not specified or "default", the fields of the CSV file are as
1153 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1157 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1159 B<record_type> is C<cust_bill> for the initial header line only. The
1160 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1161 fields are filled in.
1163 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1164 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1167 =item invnum - invoice number
1169 =item custnum - customer number
1171 =item _date - invoice date
1173 =item charged - total invoice amount
1175 =item first - customer first name
1177 =item last - customer first name
1179 =item company - company name
1181 =item address1 - address line 1
1183 =item address2 - address line 1
1193 =item pkg - line item description
1195 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1197 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1199 =item sdate - start date for recurring fee
1201 =item edate - end date for recurring fee
1205 If I<format> is "billco", the fields of the header CSV file are as follows:
1207 +-------------------------------------------------------------------+
1208 | FORMAT HEADER FILE |
1209 |-------------------------------------------------------------------|
1210 | Field | Description | Name | Type | Width |
1211 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1212 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1213 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1214 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1215 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1216 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1217 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1218 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1219 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1220 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1221 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1222 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1223 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1224 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1225 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1226 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1227 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1228 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1229 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1230 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1231 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1232 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1233 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1234 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1235 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1236 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1237 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1238 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1239 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1240 +-------+-------------------------------+------------+------+-------+
1242 If I<format> is "billco", the fields of the detail CSV file are as follows:
1244 FORMAT FOR DETAIL FILE
1246 Field | Description | Name | Type | Width
1247 1 | N/A-Leave Empty | RC | CHAR | 2
1248 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1249 3 | Account Number | TRACCTNUM | CHAR | 15
1250 4 | Invoice Number | TRINVOICE | CHAR | 15
1251 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1252 6 | Transaction Detail | DETAILS | CHAR | 100
1253 7 | Amount | AMT | NUM* | 9
1254 8 | Line Format Control** | LNCTRL | CHAR | 2
1255 9 | Grouping Code | GROUP | CHAR | 2
1256 10 | User Defined | ACCT CODE | CHAR | 15
1261 my($self, %opt) = @_;
1263 eval "use Text::CSV_XS";
1266 my $cust_main = $self->cust_main;
1268 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1270 if ( lc($opt{'format'}) eq 'billco' ) {
1273 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1275 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1277 my( $previous_balance, @unused ) = $self->previous; #previous balance
1279 my $pmt_cr_applied = 0;
1280 $pmt_cr_applied += $_->{'amount'}
1281 foreach ( $self->_items_payments, $self->_items_credits ) ;
1283 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1286 '', # 1 | N/A-Leave Empty CHAR 2
1287 '', # 2 | N/A-Leave Empty CHAR 15
1288 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1289 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1290 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1291 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1292 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1293 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1294 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1295 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1296 '', # 10 | Ancillary Billing Information CHAR 30
1297 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1298 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1301 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1304 $duedate, # 14 | Bill Due Date CHAR 10
1306 $previous_balance, # 15 | Previous Balance NUM* 9
1307 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1308 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1309 $totaldue, # 18 | Total Amt Due NUM* 9
1310 $totaldue, # 19 | Total Amt Due NUM* 9
1311 '', # 20 | 30 Day Aging NUM* 9
1312 '', # 21 | 60 Day Aging NUM* 9
1313 '', # 22 | 90 Day Aging NUM* 9
1314 'N', # 23 | Y/N CHAR 1
1315 '', # 24 | Remittance automation CHAR 100
1316 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1317 $self->custnum, # 26 | Customer Reference Number CHAR 15
1318 '0', # 27 | Federal Tax*** NUM* 9
1319 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1320 '0', # 29 | Other Taxes & Fees*** NUM* 9
1329 time2str("%x", $self->_date),
1330 sprintf("%.2f", $self->charged),
1331 ( map { $cust_main->getfield($_) }
1332 qw( first last company address1 address2 city state zip country ) ),
1334 ) or die "can't create csv";
1337 my $header = $csv->string. "\n";
1340 if ( lc($opt{'format'}) eq 'billco' ) {
1343 foreach my $item ( $self->_items_pkg ) {
1346 '', # 1 | N/A-Leave Empty CHAR 2
1347 '', # 2 | N/A-Leave Empty CHAR 15
1348 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1349 $self->invnum, # 4 | Invoice Number CHAR 15
1350 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1351 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1352 $item->{'amount'}, # 7 | Amount NUM* 9
1353 '', # 8 | Line Format Control** CHAR 2
1354 '', # 9 | Grouping Code CHAR 2
1355 '', # 10 | User Defined CHAR 15
1358 $detail .= $csv->string. "\n";
1364 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1366 my($pkg, $setup, $recur, $sdate, $edate);
1367 if ( $cust_bill_pkg->pkgnum ) {
1369 ($pkg, $setup, $recur, $sdate, $edate) = (
1370 $cust_bill_pkg->part_pkg->pkg,
1371 ( $cust_bill_pkg->setup != 0
1372 ? sprintf("%.2f", $cust_bill_pkg->setup )
1374 ( $cust_bill_pkg->recur != 0
1375 ? sprintf("%.2f", $cust_bill_pkg->recur )
1377 ( $cust_bill_pkg->sdate
1378 ? time2str("%x", $cust_bill_pkg->sdate)
1380 ($cust_bill_pkg->edate
1381 ?time2str("%x", $cust_bill_pkg->edate)
1385 } else { #pkgnum tax
1386 next unless $cust_bill_pkg->setup != 0;
1387 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1388 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1390 ($pkg, $setup, $recur, $sdate, $edate) =
1391 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1397 ( map { '' } (1..11) ),
1398 ($pkg, $setup, $recur, $sdate, $edate)
1399 ) or die "can't create csv";
1401 $detail .= $csv->string. "\n";
1407 ( $header, $detail );
1413 Pays this invoice with a compliemntary payment. If there is an error,
1414 returns the error, otherwise returns false.
1420 my $cust_pay = new FS::cust_pay ( {
1421 'invnum' => $self->invnum,
1422 'paid' => $self->owed,
1425 'payinfo' => $self->cust_main->payinfo,
1433 Attempts to pay this invoice with a credit card payment via a
1434 Business::OnlinePayment realtime gateway. See
1435 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1436 for supported processors.
1442 $self->realtime_bop( 'CC', @_ );
1447 Attempts to pay this invoice with an electronic check (ACH) payment via a
1448 Business::OnlinePayment realtime gateway. See
1449 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1450 for supported processors.
1456 $self->realtime_bop( 'ECHECK', @_ );
1461 Attempts to pay this invoice with phone bill (LEC) payment via a
1462 Business::OnlinePayment realtime gateway. See
1463 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1464 for supported processors.
1470 $self->realtime_bop( 'LEC', @_ );
1474 my( $self, $method ) = @_;
1476 my $cust_main = $self->cust_main;
1477 my $balance = $cust_main->balance;
1478 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1479 $amount = sprintf("%.2f", $amount);
1480 return "not run (balance $balance)" unless $amount > 0;
1482 my $description = 'Internet Services';
1483 if ( $conf->exists('business-onlinepayment-description') ) {
1484 my $dtempl = $conf->config('business-onlinepayment-description');
1486 my $agent_obj = $cust_main->agent
1487 or die "can't retreive agent for $cust_main (agentnum ".
1488 $cust_main->agentnum. ")";
1489 my $agent = $agent_obj->agent;
1490 my $pkgs = join(', ',
1491 map { $_->part_pkg->pkg }
1492 grep { $_->pkgnum } $self->cust_bill_pkg
1494 $description = eval qq("$dtempl");
1497 $cust_main->realtime_bop($method, $amount,
1498 'description' => $description,
1499 'invnum' => $self->invnum,
1504 =item batch_card OPTION => VALUE...
1506 Adds a payment for this invoice to the pending credit card batch (see
1507 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1508 runs the payment using a realtime gateway.
1513 my ($self, %options) = @_;
1514 my $cust_main = $self->cust_main;
1516 $options{invnum} = $self->invnum;
1518 $cust_main->batch_card(%options);
1521 sub _agent_template {
1523 $self->cust_main->agent_template;
1526 sub _agent_invoice_from {
1528 $self->cust_main->agent_invoice_from;
1531 =item print_text [ TIME [ , TEMPLATE ] ]
1533 Returns an text invoice, as a list of lines.
1535 TIME an optional value used to control the printing of overdue messages. The
1536 default is now. It isn't the date of the invoice; that's the `_date' field.
1537 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1538 L<Time::Local> and L<Date::Parse> for conversion functions.
1543 my( $self, $today, $template ) = @_;
1545 my %params = ( 'format' => 'template' );
1546 $params{'time'} = $today if $today;
1547 $params{'template'} = $template if $template;
1549 $self->print_generic( %params );
1552 =item print_latex [ TIME [ , TEMPLATE ] ]
1554 Internal method - returns a filename of a filled-in LaTeX template for this
1555 invoice (Note: add ".tex" to get the actual filename), and a filename of
1556 an associated logo (with the .eps extension included).
1558 See print_ps and print_pdf for methods that return PostScript and PDF output.
1560 TIME an optional value used to control the printing of overdue messages. The
1561 default is now. It isn't the date of the invoice; that's the `_date' field.
1562 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1563 L<Time::Local> and L<Date::Parse> for conversion functions.
1569 my( $self, $today, $template ) = @_;
1571 my %params = ( 'format' => 'latex' );
1572 $params{'time'} = $today if $today;
1573 $params{'template'} = $template if $template;
1575 $template ||= $self->_agent_template;
1577 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1578 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1582 ) or die "can't open temp file: $!\n";
1584 if ($template && $conf->exists("logo_${template}.eps")) {
1585 print $lh $conf->config_binary("logo_${template}.eps")
1586 or die "can't write temp file: $!\n";
1588 print $lh $conf->config_binary('logo.eps')
1589 or die "can't write temp file: $!\n";
1592 $params{'logo_file'} = $lh->filename;
1594 my @filled_in = $self->print_generic( %params );
1596 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1600 ) or die "can't open temp file: $!\n";
1601 print $fh join('', @filled_in );
1604 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1605 return ($1, $params{'logo_file'});
1609 =item print_generic OPTIONS_HASH
1611 Internal method - returns a filled-in template for this invoice as a scalar.
1613 See print_ps and print_pdf for methods that return PostScript and PDF output.
1615 Non optional options include
1616 format - latex, html, template
1618 Optional options include
1620 template - a value used as a suffix for a configuration template
1622 time - a value used to control the printing of overdue messages. The
1623 default is now. It isn't the date of the invoice; that's the `_date' field.
1624 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1625 L<Time::Local> and L<Date::Parse> for conversion functions.
1633 my( $self, %params ) = @_;
1634 my $today = $params{today} ? $params{today} : time;
1635 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1638 my $format = $params{format};
1639 die "Unknown format: $format"
1640 unless $format =~ /^(latex|html|template)$/;
1642 my $cust_main = $self->cust_main;
1643 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1644 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1647 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1648 'html' => [ '<%=', '%>' ],
1649 'template' => [ '{', '}' ],
1652 #create the template
1653 my $template = $params{template} ? $params{template} : $self->_agent_template;
1654 my $templatefile = "invoice_$format";
1655 $templatefile .= "_$template"
1656 if length($template);
1657 my @invoice_template = map "$_\n", $conf->config($templatefile)
1658 or die "cannot load config data $templatefile";
1661 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1662 #change this to a die when the old code is removed
1663 warn "old-style invoice template $templatefile; ".
1664 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1665 $old_latex = 'true';
1666 @invoice_template = _translate_old_latex_format(@invoice_template);
1669 my $text_template = new Text::Template(
1671 SOURCE => \@invoice_template,
1672 DELIMITERS => $delimiters{$format},
1675 $text_template->compile()
1676 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1679 # additional substitution could possibly cause breakage in existing templates
1680 my %convert_maps = (
1682 'notes' => sub { map "$_", @_ },
1683 'footer' => sub { map "$_", @_ },
1684 'smallfooter' => sub { map "$_", @_ },
1685 'returnaddress' => sub { map "$_", @_ },
1686 'coupon' => sub { map "$_", @_ },
1692 s/%%(.*)$/<!-- $1 -->/g;
1693 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1694 s/\\begin\{enumerate\}/<ol>/g;
1696 s/\\end\{enumerate\}/<\/ol>/g;
1697 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1706 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1708 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1713 s/\\\\\*?\s*$/<BR>/;
1714 s/\\hyphenation\{[\w\s\-]+}//;
1718 'coupon' => sub { "" },
1725 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1726 s/\\begin\{enumerate\}//g;
1728 s/\\end\{enumerate\}//g;
1729 s/\\textbf\{(.*)\}/$1/g;
1736 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1738 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1743 s/\\\\\*?\s*$/\n/; # dubious
1744 s/\\hyphenation\{[\w\s\-]+}//;
1748 'coupon' => sub { "" },
1753 # hashes for differing output formats
1754 my %nbsps = ( 'latex' => '~',
1755 'html' => '', # '&nbps;' would be nice
1756 'template' => '', # not used
1758 my $nbsp = $nbsps{$format};
1760 my %escape_functions = ( 'latex' => \&_latex_escape,
1761 'html' => \&encode_entities,
1762 'template' => sub { shift },
1764 my $escape_function = $escape_functions{$format};
1766 my %date_formats = ( 'latex' => '%b %o, %Y',
1767 'html' => '%b %o, %Y',
1770 my $date_format = $date_formats{$format};
1772 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1774 'html' => sub { return '<b>'. shift(). '</b>'
1776 'template' => sub { shift },
1778 my $embolden_function = $embolden_functions{$format};
1781 # generate template variables
1784 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1788 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1794 $returnaddress = join("\n",
1795 $conf->config_orbase("invoice_${format}returnaddress", $template)
1798 } elsif ( grep /\S/,
1799 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1801 my $convert_map = $convert_maps{$format}{'returnaddress'};
1804 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1809 } elsif ( grep /\S/, $conf->config('company_address') ) {
1811 my $convert_map = $convert_maps{$format}{'returnaddress'};
1812 $returnaddress = join( "\n", &$convert_map(
1813 map { s/( {2,})/'~' x length($1)/eg;
1817 ( $conf->config('company_name'),
1818 $conf->config('company_address'),
1825 my $warning = "Couldn't find a return address; ".
1826 "do you need to set the company_address configuration value?";
1828 $returnaddress = $nbsp;
1829 #$returnaddress = $warning;
1833 my %invoice_data = (
1834 'company_name' => scalar( $conf->config('company_name') ),
1835 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1836 'custnum' => $self->custnum,
1837 'invnum' => $self->invnum,
1838 'date' => time2str($date_format, $self->_date),
1839 'today' => time2str('%b %o, %Y', $today),
1840 'agent' => &$escape_function($cust_main->agent->agent),
1841 'agent_custid' => &$escape_function($cust_main->agent_custid),
1842 'payname' => &$escape_function($cust_main->payname),
1843 'company' => &$escape_function($cust_main->company),
1844 'address1' => &$escape_function($cust_main->address1),
1845 'address2' => &$escape_function($cust_main->address2),
1846 'city' => &$escape_function($cust_main->city),
1847 'state' => &$escape_function($cust_main->state),
1848 'zip' => &$escape_function($cust_main->zip),
1849 'returnaddress' => $returnaddress,
1851 'terms' => $self->terms,
1852 'template' => $params{'template'},
1853 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1854 # better hang on to conf_dir for a while
1855 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1858 'current_charges' => sprintf("%.2f", $self->charged),
1859 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1860 'ship_enable' => $conf->exists('invoice-ship_address'),
1861 'unitprices' => $conf->exists('invoice-unitprice'),
1864 my $countrydefault = $conf->config('countrydefault') || 'US';
1865 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1866 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1867 my $method = $prefix.$_;
1868 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1870 $invoice_data{'ship_country'} = ''
1871 if ( $invoice_data{'ship_country'} eq $countrydefault );
1873 $invoice_data{'cid'} = $params{'cid'}
1876 if ( $cust_main->country eq $countrydefault ) {
1877 $invoice_data{'country'} = '';
1879 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1883 $invoice_data{'address'} = \@address;
1885 $cust_main->payname.
1886 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1887 ? " (P.O. #". $cust_main->payinfo. ")"
1891 push @address, $cust_main->company
1892 if $cust_main->company;
1893 push @address, $cust_main->address1;
1894 push @address, $cust_main->address2
1895 if $cust_main->address2;
1897 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1898 push @address, $invoice_data{'country'}
1899 if $invoice_data{'country'};
1901 while (scalar(@address) < 5);
1903 $invoice_data{'logo_file'} = $params{'logo_file'}
1904 if $params{'logo_file'};
1906 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1907 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1908 #my $balance_due = $self->owed + $pr_total - $cr_total;
1909 my $balance_due = $self->owed + $pr_total;
1910 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1911 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1913 #do variable substitution in notes, footer, smallfooter
1914 foreach my $include (qw( notes footer smallfooter coupon )) {
1916 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1919 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1921 @inc_src = $conf->config($inc_file);
1925 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1927 my $convert_map = $convert_maps{$format}{$include};
1929 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1930 s/--\@\]/$delimiters{$format}[1]/g;
1933 &$convert_map( $conf->config($inc_file) );
1937 my $inc_tt = new Text::Template (
1939 SOURCE => [ map "$_\n", @inc_src ],
1940 DELIMITERS => $delimiters{$format},
1941 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1943 unless ( $inc_tt->compile() ) {
1944 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1945 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1949 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1951 $invoice_data{$include} =~ s/\n+$//
1952 if ($format eq 'latex');
1955 $invoice_data{'po_line'} =
1956 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1957 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1960 my %money_chars = ( 'latex' => '',
1961 'html' => $conf->config('money_char') || '$',
1964 my $money_char = $money_chars{$format};
1966 my %other_money_chars = ( 'latex' => '\dollar ',
1967 'html' => $conf->config('money_char') || '$',
1970 my $other_money_char = $other_money_chars{$format};
1972 my @detail_items = ();
1973 my @total_items = ();
1977 $invoice_data{'detail_items'} = \@detail_items;
1978 $invoice_data{'total_items'} = \@total_items;
1979 $invoice_data{'buf'} = \@buf;
1980 $invoice_data{'sections'} = \@sections;
1982 my $previous_section = { 'description' => 'Previous Charges',
1983 'subtotal' => $other_money_char.
1984 sprintf('%.2f', $pr_total),
1988 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1989 'subtotal' => $taxtotal }; # adjusted below
1991 my $adjusttotal = 0;
1992 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
1993 'subtotal' => 0 }; # adjusted below
1995 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
1996 if ( $multisection ) {
1997 push @sections, $self->_items_sections;
1999 push @sections, { 'description' => '', 'subtotal' => '' };
2002 foreach my $line_item ( $conf->exists('disable_previous_balance')
2004 : $self->_items_previous
2008 ext_description => [],
2010 $detail->{'ref'} = $line_item->{'pkgnum'};
2011 $detail->{'quantity'} = 1;
2012 $detail->{'section'} = $previous_section;
2013 $detail->{'description'} = &$escape_function($line_item->{'description'});
2014 if ( exists $line_item->{'ext_description'} ) {
2015 @{$detail->{'ext_description'}} = map {
2016 &$escape_function($_);
2017 } @{$line_item->{'ext_description'}};
2019 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2020 $line_item->{'amount'};
2021 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2023 push @detail_items, $detail;
2024 push @buf, [ $detail->{'description'},
2025 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2029 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2030 push @buf, ['','-----------'];
2031 push @buf, [ 'Total Previous Balance',
2032 $money_char. sprintf("%10.2f", $pr_total) ];
2036 foreach my $section (@sections) {
2038 $section->{'subtotal'} = $other_money_char.
2039 sprintf('%.2f', $section->{'subtotal'})
2042 if ( $section->{'description'} ) {
2043 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2049 $options{'section'} = $section if $multisection;
2050 $options{'format'} = $format;
2051 $options{'escape_function'} = $escape_function;
2053 foreach my $line_item ( $self->_items_pkg(%options) ) {
2055 ext_description => [],
2057 $detail->{'ref'} = $line_item->{'pkgnum'};
2058 $detail->{'quantity'} = $line_item->{'quantity'};
2059 $detail->{'section'} = $section;
2060 $detail->{'description'} = &$escape_function($line_item->{'description'});
2061 if ( exists $line_item->{'ext_description'} ) {
2062 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2064 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2065 $line_item->{'amount'};
2066 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2067 $line_item->{'unit_amount'};
2068 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2070 push @detail_items, $detail;
2071 push @buf, ( [ $detail->{'description'},
2072 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2074 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2078 if ( $section->{'description'} ) {
2079 push @buf, ( ['','-----------'],
2080 [ $section->{'description'}. ' sub-total',
2081 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2090 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2091 unshift @sections, $previous_section if $pr_total;
2094 foreach my $tax ( $self->_items_tax ) {
2096 $total->{'total_item'} = &$escape_function($tax->{'description'});
2097 $taxtotal += $tax->{'amount'};
2098 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2099 if ( $multisection ) {
2100 my $money = $old_latex ? '' : $money_char;
2101 push @detail_items, {
2102 ext_description => [],
2105 description => &$escape_function($tax->{'description'}),
2106 amount => $money. $tax->{'amount'},
2108 section => $tax_section,
2111 push @total_items, $total;
2113 push @buf,[ $total->{'total_item'},
2114 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2121 $total->{'total_item'} = 'Sub-total';
2122 $total->{'total_amount'} =
2123 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2125 if ( $multisection ) {
2126 $tax_section->{'subtotal'} = $other_money_char.
2127 sprintf('%.2f', $taxtotal);
2128 $tax_section->{'pretotal'} = 'New charges sub-total '.
2129 $total->{'total_amount'};
2130 push @sections, $tax_section if $taxtotal;
2132 unshift @total_items, $total;
2135 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2137 push @buf,['','-----------'];
2138 push @buf,[( $conf->exists('disable_previous_balance')
2140 : 'Total New Charges'
2142 $money_char. sprintf("%10.2f",$self->charged) ];
2147 $total->{'total_item'} = &$embolden_function('Total');
2148 $total->{'total_amount'} =
2149 &$embolden_function(
2152 $self->charged + ( $conf->exists('disable_previous_balance')
2158 if ( $multisection ) {
2159 $adjust_section->{'pretotal'} = 'New charges total '.
2160 $total->{'total_amount'};
2162 push @total_items, $total;
2164 push @buf,['','-----------'];
2165 push @buf,['Total Charges',
2167 sprintf( '%10.2f', $self->charged +
2168 ( $conf->exists('disable_previous_balance')
2177 unless ( $conf->exists('disable_previous_balance') ) {
2178 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2181 my $credittotal = 0;
2182 foreach my $credit ( $self->_items_credits ) {
2184 $total->{'total_item'} = &$escape_function($credit->{'description'});
2185 $credittotal += $credit->{'amount'};
2186 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2187 $adjusttotal += $credit->{'amount'};
2188 if ( $multisection ) {
2189 my $money = $old_latex ? '' : $money_char;
2190 push @detail_items, {
2191 ext_description => [],
2194 description => &$escape_function($credit->{'description'}),
2195 amount => $money. $credit->{'amount'},
2197 section => $adjust_section,
2200 push @total_items, $total;
2203 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2206 foreach ( $self->cust_credited ) {
2208 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2210 my $reason = substr($_->cust_credit->reason,0,32);
2211 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2212 $reason = " ($reason) " if $reason;
2214 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2215 $money_char. sprintf("%10.2f",$_->amount)
2220 my $paymenttotal = 0;
2221 foreach my $payment ( $self->_items_payments ) {
2223 $total->{'total_item'} = &$escape_function($payment->{'description'});
2224 $paymenttotal += $payment->{'amount'};
2225 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2226 $adjusttotal += $payment->{'amount'};
2227 if ( $multisection ) {
2228 my $money = $old_latex ? '' : $money_char;
2229 push @detail_items, {
2230 ext_description => [],
2233 description => &$escape_function($payment->{'description'}),
2234 amount => $money. $payment->{'amount'},
2236 section => $adjust_section,
2239 push @total_items, $total;
2241 push @buf, [ $payment->{'description'},
2242 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2245 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2247 if ( $multisection ) {
2248 $adjust_section->{'subtotal'} = $other_money_char.
2249 sprintf('%.2f', $adjusttotal);
2250 push @sections, $adjust_section;
2255 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2256 $total->{'total_amount'} =
2257 &$embolden_function(
2258 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2260 if ( $multisection ) {
2261 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2262 $total->{'total_amount'};
2264 push @total_items, $total;
2266 push @buf,['','-----------'];
2267 push @buf,[$self->balance_due_msg, $money_char.
2268 sprintf("%10.2f", $balance_due ) ];
2274 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2275 /invoice_lines\((\d*)\)/;
2276 $invoice_lines += $1 || scalar(@buf);
2279 die "no invoice_lines() functions in template?"
2280 if ( $format eq 'template' && !$wasfunc );
2282 if ($format eq 'template') {
2284 if ( $invoice_lines ) {
2285 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2286 $invoice_data{'total_pages'}++
2287 if scalar(@buf) % $invoice_lines;
2290 #setup subroutine for the template
2291 sub FS::cust_bill::_template::invoice_lines {
2292 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2294 scalar(@FS::cust_bill::_template::buf)
2295 ? shift @FS::cust_bill::_template::buf
2304 push @collect, split("\n",
2305 $text_template->fill_in( HASH => \%invoice_data,
2306 PACKAGE => 'FS::cust_bill::_template'
2309 $FS::cust_bill::_template::page++;
2311 map "$_\n", @collect;
2313 warn "filling in template for invoice ". $self->invnum. "\n"
2315 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2318 $text_template->fill_in(HASH => \%invoice_data);
2322 =item print_ps [ TIME [ , TEMPLATE ] ]
2324 Returns an postscript invoice, as a scalar.
2326 TIME an optional value used to control the printing of overdue messages. The
2327 default is now. It isn't the date of the invoice; that's the `_date' field.
2328 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2329 L<Time::Local> and L<Date::Parse> for conversion functions.
2336 my ($file, $lfile) = $self->print_latex(@_);
2337 my $ps = generate_ps($file);
2343 =item print_pdf [ TIME [ , TEMPLATE ] ]
2345 Returns an PDF invoice, as a scalar.
2347 TIME an optional value used to control the printing of overdue messages. The
2348 default is now. It isn't the date of the invoice; that's the `_date' field.
2349 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2350 L<Time::Local> and L<Date::Parse> for conversion functions.
2357 my ($file, $lfile) = $self->print_latex(@_);
2358 my $pdf = generate_pdf($file);
2364 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2366 Returns an HTML invoice, as a scalar.
2368 TIME an optional value used to control the printing of overdue messages. The
2369 default is now. It isn't the date of the invoice; that's the `_date' field.
2370 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2371 L<Time::Local> and L<Date::Parse> for conversion functions.
2373 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2374 when emailing the invoice as part of a multipart/related MIME email.
2379 my( $self, $today, $template, $cid ) = @_;
2381 my %params = ( 'format' => 'html' );
2382 $params{'time'} = $today if $today;
2383 $params{'template'} = $template if $template;
2384 $params{'cid'} = $cid if $cid;
2386 $self->print_generic( %params );
2389 # quick subroutine for print_latex
2391 # There are ten characters that LaTeX treats as special characters, which
2392 # means that they do not simply typeset themselves:
2393 # # $ % & ~ _ ^ \ { }
2395 # TeX ignores blanks following an escaped character; if you want a blank (as
2396 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2400 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2401 $value =~ s/([<>])/\$$1\$/g;
2405 #utility methods for print_*
2407 sub _translate_old_latex_format {
2408 warn "_translate_old_latex_format called\n"
2415 if ( $line =~ /^%%Detail\s*$/ ) {
2417 push @template, q![@--!,
2418 q! foreach my $_tr_line (@detail_items) {!,
2419 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2420 q! $_tr_line->{'description'} .= !,
2421 q! "\\tabularnewline\n~~".!,
2422 q! join( "\\tabularnewline\n~~",!,
2423 q! @{$_tr_line->{'ext_description'}}!,
2427 while ( ( my $line_item_line = shift )
2428 !~ /^%%EndDetail\s*$/ ) {
2429 $line_item_line =~ s/'/\\'/g; # nice LTS
2430 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2431 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2432 push @template, " \$OUT .= '$line_item_line';";
2435 push @template, '}',
2438 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2440 push @template, '[@--',
2441 ' foreach my $_tr_line (@total_items) {';
2443 while ( ( my $total_item_line = shift )
2444 !~ /^%%EndTotalDetails\s*$/ ) {
2445 $total_item_line =~ s/'/\\'/g; # nice LTS
2446 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2447 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2448 push @template, " \$OUT .= '$total_item_line';";
2451 push @template, '}',
2455 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2456 push @template, $line;
2462 warn "$_\n" foreach @template;
2471 #check for an invoice- specific override (eventually)
2473 #check for a customer- specific override
2474 return $self->cust_main->invoice_terms
2475 if $self->cust_main->invoice_terms;
2477 #use configured default or default default
2478 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2484 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2485 $duedate = $self->_date() + ( $1 * 86400 );
2492 $self->due_date ? time2str(shift, $self->due_date) : '';
2495 sub balance_due_msg {
2497 my $msg = 'Balance Due';
2498 return $msg unless $self->terms;
2499 if ( $self->due_date ) {
2500 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2501 } elsif ( $self->terms ) {
2502 $msg .= ' - '. $self->terms;
2507 sub balance_due_date {
2510 if ( $conf->exists('invoice_default_terms')
2511 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2512 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2517 =item invnum_date_pretty
2519 Returns a string with the invoice number and date, for example:
2520 "Invoice #54 (3/20/2008)"
2524 sub invnum_date_pretty {
2526 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2529 sub _items_sections {
2533 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2535 if ( $cust_bill_pkg->pkgnum > 0 ) {
2537 my $desc = $cust_bill_pkg->part_pkg->categoryname;
2539 $s{$desc} += $cust_bill_pkg->setup
2540 if ( $cust_bill_pkg->setup != 0 );
2542 $s{$desc} += $cust_bill_pkg->recur
2543 if ( $cust_bill_pkg->recur != 0 );
2549 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2556 #my @display = scalar(@_)
2558 # : qw( _items_previous _items_pkg );
2559 # #: qw( _items_pkg );
2560 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2561 my @display = qw( _items_previous _items_pkg );
2564 foreach my $display ( @display ) {
2565 push @b, $self->$display(@_);
2570 sub _items_previous {
2572 my $cust_main = $self->cust_main;
2573 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2575 foreach ( @pr_cust_bill ) {
2577 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2578 ' ('. time2str('%x',$_->_date). ')',
2579 #'pkgpart' => 'N/A',
2581 'amount' => sprintf("%.2f", $_->owed),
2587 # 'description' => 'Previous Balance',
2588 # #'pkgpart' => 'N/A',
2589 # 'pkgnum' => 'N/A',
2590 # 'amount' => sprintf("%10.2f", $pr_total ),
2591 # 'ext_description' => [ map {
2592 # "Invoice ". $_->invnum.
2593 # " (". time2str("%x",$_->_date). ") ".
2594 # sprintf("%10.2f", $_->owed)
2595 # } @pr_cust_bill ],
2603 my $section = delete $options{'section'};
2605 grep { $_->pkgnum &&
2607 ? $_->part_pkg->categoryname eq $section->{'description'}
2610 } $self->cust_bill_pkg;
2611 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2615 return 0 unless $a cmp $b;
2616 return -1 if $b eq 'Tax';
2617 return 1 if $a eq 'Tax';
2618 return -1 if $b eq 'Other surcharges';
2619 return 1 if $a eq 'Other surcharges';
2625 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2626 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2629 sub _items_cust_bill_pkg {
2631 my $cust_bill_pkg = shift;
2634 my $format = $opt{format} || '';
2635 my $escape_function = $opt{escape_function} || sub { shift };
2638 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2640 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2642 my $desc = $cust_bill_pkg->desc;
2644 my %details_opt = ( 'format' => $format,
2645 'escape_function' => $escape_function,
2648 if ( $cust_bill_pkg->pkgnum > 0 ) {
2650 if ( $cust_bill_pkg->setup != 0 ) {
2652 my $description = $desc;
2653 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2655 my @d = map &{$escape_function}($_),
2656 $cust_pkg->h_labels_short($self->_date);
2657 push @d, $cust_bill_pkg->details(%details_opt)
2658 if $cust_bill_pkg->recur == 0;
2661 description => $description,
2662 #pkgpart => $part_pkg->pkgpart,
2663 pkgnum => $cust_bill_pkg->pkgnum,
2664 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2665 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2666 quantity => $cust_bill_pkg->quantity,
2667 ext_description => \@d,
2671 if ( $cust_bill_pkg->recur != 0 ) {
2673 my $description = $desc;
2674 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2675 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2676 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2679 #at least until cust_bill_pkg has "past" ranges in addition to
2680 #the "future" sdate/edate ones... see #3032
2681 my @d = map &{$escape_function}($_),
2682 $cust_pkg->h_labels_short($self->_date);
2683 #$cust_bill_pkg->edate,
2684 #$cust_bill_pkg->sdate),
2685 push @d, $cust_bill_pkg->details(%details_opt);
2688 description => $description,
2689 #pkgpart => $part_pkg->pkgpart,
2690 pkgnum => $cust_bill_pkg->pkgnum,
2691 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2692 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2693 quantity => $cust_bill_pkg->quantity,
2694 ext_description => \@d,
2699 } else { #pkgnum tax or one-shot line item (??)
2701 if ( $cust_bill_pkg->setup != 0 ) {
2703 'description' => $desc,
2704 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2707 if ( $cust_bill_pkg->recur != 0 ) {
2709 'description' => "$desc (".
2710 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2711 time2str("%x", $cust_bill_pkg->edate). ')',
2712 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2724 sub _items_credits {
2729 foreach ( $self->cust_credited ) {
2731 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2733 my $reason = $_->cust_credit->reason;
2734 #my $reason = substr($_->cust_credit->reason,0,32);
2735 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2736 $reason = " ($reason) " if $reason;
2738 #'description' => 'Credit ref\#'. $_->crednum.
2739 # " (". time2str("%x",$_->cust_credit->_date) .")".
2741 'description' => 'Credit applied '.
2742 time2str("%x",$_->cust_credit->_date). $reason,
2743 'amount' => sprintf("%.2f",$_->amount),
2746 #foreach ( @cr_cust_credit ) {
2748 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2749 # $money_char. sprintf("%10.2f",$_->credited)
2757 sub _items_payments {
2761 #get & print payments
2762 foreach ( $self->cust_bill_pay ) {
2764 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2767 'description' => "Payment received ".
2768 time2str("%x",$_->cust_pay->_date ),
2769 'amount' => sprintf("%.2f", $_->amount )
2788 sub process_reprint {
2789 process_re_X('print', @_);
2796 sub process_reemail {
2797 process_re_X('email', @_);
2805 process_re_X('fax', @_);
2808 use Storable qw(thaw);
2812 my( $method, $job ) = ( shift, shift );
2813 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2815 my $param = thaw(decode_base64(shift));
2816 warn Dumper($param) if $DEBUG;
2827 my($method, $job, %param ) = @_;
2829 warn "re_X $method for job $job with param:\n".
2830 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2833 #some false laziness w/search/cust_bill.html
2835 my $orderby = 'ORDER BY cust_bill._date';
2837 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2839 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2841 my @cust_bill = qsearch( {
2842 #'select' => "cust_bill.*",
2843 'table' => 'cust_bill',
2844 'addl_from' => $addl_from,
2846 'extra_sql' => $extra_sql,
2847 'order_by' => $orderby,
2851 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2854 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2855 foreach my $cust_bill ( @cust_bill ) {
2856 $cust_bill->$method();
2858 if ( $job ) { #progressbar foo
2860 if ( time - $min_sec > $last ) {
2861 my $error = $job->update_statustext(
2862 int( 100 * $num / scalar(@cust_bill) )
2864 die $error if $error;
2875 =head1 CLASS METHODS
2881 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2887 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2892 Returns an SQL fragment to retreive the net amount (charged minus credited).
2898 'charged - '. $class->credited_sql;
2903 Returns an SQL fragment to retreive the amount paid against this invoice.
2909 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2910 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2915 Returns an SQL fragment to retreive the amount credited against this invoice.
2921 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2922 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2925 =item search_sql HASHREF
2927 Class method which returns an SQL WHERE fragment to search for parameters
2928 specified in HASHREF. Valid parameters are
2934 Epoch date (UNIX timestamp) setting a lower bound for _date values
2938 Epoch date (UNIX timestamp) setting an upper bound for _date values
2952 =item newest_percust
2956 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2961 my($class, $param) = @_;
2963 warn "$me search_sql called with params: \n".
2964 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2969 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2970 push @search, "cust_bill._date >= $1";
2972 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2973 push @search, "cust_bill._date < $1";
2975 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2976 push @search, "cust_bill.invnum >= $1";
2978 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2979 push @search, "cust_bill.invnum <= $1";
2981 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2982 push @search, "cust_main.agentnum = $1";
2985 push @search, '0 != '. FS::cust_bill->owed_sql
2986 if $param->{'open'};
2988 push @search, '0 != '. FS::cust_bill->net_sql
2991 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2992 if $param->{'days'};
2994 if ( $param->{'newest_percust'} ) {
2996 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
2997 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2999 my @newest_where = map { my $x = $_;
3000 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3003 grep ! /^cust_main./, @search;
3004 my $newest_where = scalar(@newest_where)
3005 ? ' AND '. join(' AND ', @newest_where)
3009 push @search, "cust_bill._date = (
3010 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3011 WHERE newest_cust_bill.custnum = cust_bill.custnum
3017 my $curuser = $FS::CurrentUser::CurrentUser;
3018 if ( $curuser->username eq 'fs_queue'
3019 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3021 my $newuser = qsearchs('access_user', {
3022 'username' => $username,
3026 $curuser = $newuser;
3028 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3032 push @search, $curuser->agentnums_sql;
3034 join(' AND ', @search );
3046 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3047 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base