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.
230 { 'table' => 'cust_bill_pkg',
231 'hashref' => { 'invnum' => $self->invnum },
232 'order_by' => 'ORDER BY billpkgnum',
239 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
246 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
248 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
251 =item open_cust_bill_pkg
253 Returns the open line items for this invoice.
255 Note that cust_bill_pkg with both setup and recur fees are returned as two
256 separate line items, each with only one fee.
260 # modeled after cust_main::open_cust_bill
261 sub open_cust_bill_pkg {
264 # grep { $_->owed > 0 } $self->cust_bill_pkg
266 my %other = ( 'recur' => 'setup',
267 'setup' => 'recur', );
269 foreach my $field ( qw( recur setup )) {
270 push @open, map { $_->set( $other{$field}, 0 ); $_; }
271 grep { $_->owed($field) > 0 }
272 $self->cust_bill_pkg;
278 =item cust_bill_event
280 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
284 sub cust_bill_event {
286 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
289 =item num_cust_bill_event
291 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
295 sub num_cust_bill_event {
298 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
299 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
300 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
301 $sth->fetchrow_arrayref->[0];
306 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
310 #false laziness w/cust_pkg.pm
314 'table' => 'cust_event',
315 'addl_from' => 'JOIN part_event USING ( eventpart )',
316 'hashref' => { 'tablenum' => $self->invnum },
317 'extra_sql' => " AND eventtable = 'cust_bill' ",
323 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
327 #false laziness w/cust_pkg.pm
331 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
332 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
333 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
334 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
335 $sth->fetchrow_arrayref->[0];
340 Returns the customer (see L<FS::cust_main>) for this invoice.
346 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
349 =item cust_suspend_if_balance_over AMOUNT
351 Suspends the customer associated with this invoice if the total amount owed on
352 this invoice and all older invoices is greater than the specified amount.
354 Returns a list: an empty list on success or a list of errors.
358 sub cust_suspend_if_balance_over {
359 my( $self, $amount ) = ( shift, shift );
360 my $cust_main = $self->cust_main;
361 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
364 $cust_main->suspend(@_);
370 Depreciated. See the cust_credited method.
372 #Returns a list consisting of the total previous credited (see
373 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
374 #outstanding credits (FS::cust_credit objects).
380 croak "FS::cust_bill->cust_credit depreciated; see ".
381 "FS::cust_bill->cust_credit_bill";
384 #my @cust_credit = sort { $a->_date <=> $b->_date }
385 # grep { $_->credited != 0 && $_->_date < $self->_date }
386 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
388 #foreach (@cust_credit) { $total += $_->credited; }
389 #$total, @cust_credit;
394 Depreciated. See the cust_bill_pay method.
396 #Returns all payments (see L<FS::cust_pay>) for this invoice.
402 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
404 #sort { $a->_date <=> $b->_date }
405 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
411 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
417 sort { $a->_date <=> $b->_date }
418 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
423 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
429 sort { $a->_date <=> $b->_date }
430 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
436 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
443 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
445 foreach (@taxlines) { $total += $_->setup; }
451 Returns the amount owed (still outstanding) on this invoice, which is charged
452 minus all payment applications (see L<FS::cust_bill_pay>) and credit
453 applications (see L<FS::cust_credit_bill>).
459 my $balance = $self->charged;
460 $balance -= $_->amount foreach ( $self->cust_bill_pay );
461 $balance -= $_->amount foreach ( $self->cust_credited );
462 $balance = sprintf( "%.2f", $balance);
463 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
467 =item apply_payments_and_credits
471 sub apply_payments_and_credits {
474 local $SIG{HUP} = 'IGNORE';
475 local $SIG{INT} = 'IGNORE';
476 local $SIG{QUIT} = 'IGNORE';
477 local $SIG{TERM} = 'IGNORE';
478 local $SIG{TSTP} = 'IGNORE';
479 local $SIG{PIPE} = 'IGNORE';
481 my $oldAutoCommit = $FS::UID::AutoCommit;
482 local $FS::UID::AutoCommit = 0;
485 $self->select_for_update; #mutex
487 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
488 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
490 while ( $self->owed > 0 and ( @payments || @credits ) ) {
493 if ( @payments && @credits ) {
495 #decide which goes first by weight of top (unapplied) line item
497 my @open_lineitems = $self->open_cust_bill_pkg;
500 max( map { $_->part_pkg->pay_weight || 0 }
505 my $max_credit_weight =
506 max( map { $_->part_pkg->credit_weight || 0 }
512 #if both are the same... payments first? it has to be something
513 if ( $max_pay_weight >= $max_credit_weight ) {
519 } elsif ( @payments ) {
521 } elsif ( @credits ) {
524 die "guru meditation #12 and 35";
527 if ( $app eq 'pay' ) {
529 my $payment = shift @payments;
531 $app = new FS::cust_bill_pay {
532 'paynum' => $payment->paynum,
533 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
536 } elsif ( $app eq 'credit' ) {
538 my $credit = shift @credits;
540 $app = new FS::cust_credit_bill {
541 'crednum' => $credit->crednum,
542 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
546 die "guru meditation #12 and 35";
549 $app->invnum( $self->invnum );
551 my $error = $app->insert;
553 $dbh->rollback if $oldAutoCommit;
554 return "Error inserting ". $app->table. " record: $error";
556 die $error if $error;
560 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
565 =item generate_email OPTION => VALUE ...
573 sender address, required
577 alternate template name, optional
581 text attachment arrayref, optional
585 email subject, optional
589 Returns an argument list to be passed to L<FS::Misc::send_email>.
600 my $me = '[FS::cust_bill::generate_email]';
603 'from' => $args{'from'},
604 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
607 if (ref($args{'to'}) eq 'ARRAY') {
608 $return{'to'} = $args{'to'};
610 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
611 $self->cust_main->invoicing_list
615 if ( $conf->exists('invoice_html') ) {
617 warn "$me creating HTML/text multipart message"
620 $return{'nobody'} = 1;
622 my $alternative = build MIME::Entity
623 'Type' => 'multipart/alternative',
624 'Encoding' => '7bit',
625 'Disposition' => 'inline'
629 if ( $conf->exists('invoice_email_pdf')
630 and scalar($conf->config('invoice_email_pdf_note')) ) {
632 warn "$me using 'invoice_email_pdf_note' in multipart message"
634 $data = [ map { $_ . "\n" }
635 $conf->config('invoice_email_pdf_note')
640 warn "$me not using 'invoice_email_pdf_note' in multipart message"
642 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
643 $data = $args{'print_text'};
645 $data = [ $self->print_text('', $args{'template'}) ];
650 $alternative->attach(
651 'Type' => 'text/plain',
652 #'Encoding' => 'quoted-printable',
653 'Encoding' => '7bit',
655 'Disposition' => 'inline',
658 $args{'from'} =~ /\@([\w\.\-]+)/;
659 my $from = $1 || 'example.com';
660 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
662 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
664 if ( defined($args{'template'}) && length($args{'template'})
665 && -e "$path/logo_". $args{'template'}. ".png"
668 $file = "$path/logo_". $args{'template'}. ".png";
670 $file = "$path/logo.png";
673 my $image = build MIME::Entity
674 'Type' => 'image/png',
675 'Encoding' => 'base64',
677 'Filename' => 'logo.png',
678 'Content-ID' => "<$content_id>",
681 $alternative->attach(
682 'Type' => 'text/html',
683 'Encoding' => 'quoted-printable',
684 'Data' => [ '<html>',
687 ' '. encode_entities($return{'subject'}),
690 ' <body bgcolor="#e8e8e8">',
691 $self->print_html('', $args{'template'}, $content_id),
695 'Disposition' => 'inline',
696 #'Filename' => 'invoice.pdf',
699 if ( $conf->exists('invoice_email_pdf') ) {
704 # multipart/alternative
710 my $related = build MIME::Entity 'Type' => 'multipart/related',
711 'Encoding' => '7bit';
713 #false laziness w/Misc::send_email
714 $related->head->replace('Content-type',
716 '; boundary="'. $related->head->multipart_boundary. '"'.
717 '; type=multipart/alternative'
720 $related->add_part($alternative);
722 $related->add_part($image);
724 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
726 $return{'mimeparts'} = [ $related, $pdf ];
730 #no other attachment:
732 # multipart/alternative
737 $return{'content-type'} = 'multipart/related';
738 $return{'mimeparts'} = [ $alternative, $image ];
739 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
740 #$return{'disposition'} = 'inline';
746 if ( $conf->exists('invoice_email_pdf') ) {
747 warn "$me creating PDF attachment"
750 #mime parts arguments a la MIME::Entity->build().
751 $return{'mimeparts'} = [
752 { $self->mimebuild_pdf('', $args{'template'}) }
756 if ( $conf->exists('invoice_email_pdf')
757 and scalar($conf->config('invoice_email_pdf_note')) ) {
759 warn "$me using 'invoice_email_pdf_note'"
761 $return{'body'} = [ map { $_ . "\n" }
762 $conf->config('invoice_email_pdf_note')
767 warn "$me not using 'invoice_email_pdf_note'"
769 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
770 $return{'body'} = $args{'print_text'};
772 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
785 Returns a list suitable for passing to MIME::Entity->build(), representing
786 this invoice as PDF attachment.
793 'Type' => 'application/pdf',
794 'Encoding' => 'base64',
795 'Data' => [ $self->print_pdf(@_) ],
796 'Disposition' => 'attachment',
797 'Filename' => 'invoice.pdf',
801 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
803 Sends this invoice to the destinations configured for this customer: sends
804 email, prints and/or faxes. See L<FS::cust_main_invoice>.
806 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
808 AGENTNUM, if specified, means that this invoice will only be sent for customers
809 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
810 single agent) or an arrayref of agentnums.
812 INVOICE_FROM, if specified, overrides the default email invoice From: address.
814 AMOUNT, if specified, only sends the invoice if the total amount owed on this
815 invoice and all older invoices is greater than the specified amount.
822 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
823 or die "invalid invoice number: " . $opt{invnum};
825 my @args = ( $opt{template}, $opt{agentnum} );
826 push @args, $opt{invoice_from}
827 if exists($opt{invoice_from}) && $opt{invoice_from};
829 my $error = $self->send( @args );
830 die $error if $error;
836 my $template = scalar(@_) ? shift : '';
837 if ( scalar(@_) && $_[0] ) {
838 my $agentnums = ref($_[0]) ? shift : [ shift ];
839 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
845 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
847 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
850 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
852 my @invoicing_list = $self->cust_main->invoicing_list;
854 #$self->email_invoice($template, $invoice_from)
855 $self->email($template, $invoice_from)
856 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
858 #$self->print_invoice($template)
859 $self->print($template)
860 if grep { $_ eq 'POST' } @invoicing_list; #postal
862 $self->fax_invoice($template)
863 if grep { $_ eq 'FAX' } @invoicing_list; #fax
869 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
873 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
875 INVOICE_FROM, if specified, overrides the default email invoice From: address.
879 sub queueable_email {
882 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
883 or die "invalid invoice number: " . $opt{invnum};
885 my @args = ( $opt{template} );
886 push @args, $opt{invoice_from}
887 if exists($opt{invoice_from}) && $opt{invoice_from};
889 my $error = $self->email( @args );
890 die $error if $error;
897 my $template = scalar(@_) ? shift : '';
901 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
903 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
904 $self->cust_main->invoicing_list;
906 #better to notify this person than silence
907 @invoicing_list = ($invoice_from) unless @invoicing_list;
909 my $error = send_email(
910 $self->generate_email(
911 'from' => $invoice_from,
912 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
913 'template' => $template,
916 die "can't email invoice: $error\n" if $error;
917 #die "$error\n" if $error;
921 =item lpr_data [ TEMPLATENAME ]
923 Returns the postscript or plaintext for this invoice as an arrayref.
925 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
930 my( $self, $template) = @_;
931 $conf->exists('invoice_latex')
932 ? [ $self->print_ps('', $template) ]
933 : [ $self->print_text('', $template) ];
936 =item print [ TEMPLATENAME ]
940 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
947 my $template = scalar(@_) ? shift : '';
949 do_print $self->lpr_data($template);
952 =item fax_invoice [ TEMPLATENAME ]
956 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
962 my $template = scalar(@_) ? shift : '';
964 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
965 unless $conf->exists('invoice_latex');
967 my $dialstring = $self->cust_main->getfield('fax');
970 my $error = send_fax( 'docdata' => $self->lpr_data($template),
971 'dialstring' => $dialstring,
973 die $error if $error;
977 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
979 Like B<send>, but only sends the invoice if it is the newest open invoice for
989 grep { $_->owed > 0 }
990 qsearch('cust_bill', {
991 'custnum' => $self->custnum,
992 #'_date' => { op=>'>', value=>$self->_date },
993 'invnum' => { op=>'>', value=>$self->invnum },
1000 =item send_csv OPTION => VALUE, ...
1002 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1006 protocol - currently only "ftp"
1012 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1013 and YYMMDDHHMMSS is a timestamp.
1015 See L</print_csv> for a description of the output format.
1020 my($self, %opt) = @_;
1024 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1025 mkdir $spooldir, 0700 unless -d $spooldir;
1027 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1028 my $file = "$spooldir/$tracctnum.csv";
1030 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1032 open(CSV, ">$file") or die "can't open $file: $!";
1040 if ( $opt{protocol} eq 'ftp' ) {
1041 eval "use Net::FTP;";
1043 $net = Net::FTP->new($opt{server}) or die @$;
1045 die "unknown protocol: $opt{protocol}";
1048 $net->login( $opt{username}, $opt{password} )
1049 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1051 $net->binary or die "can't set binary mode";
1053 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1055 $net->put($file) or die "can't put $file: $!";
1065 Spools CSV invoice data.
1071 =item format - 'default' or 'billco'
1073 =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>).
1075 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1077 =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.
1084 my($self, %opt) = @_;
1086 my $cust_main = $self->cust_main;
1088 if ( $opt{'dest'} ) {
1089 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1090 $cust_main->invoicing_list;
1091 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1092 || ! keys %invoicing_list;
1095 if ( $opt{'balanceover'} ) {
1097 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1100 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1101 mkdir $spooldir, 0700 unless -d $spooldir;
1103 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1107 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1108 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1111 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1113 open(CSV, ">>$file") or die "can't open $file: $!";
1114 flock(CSV, LOCK_EX);
1119 if ( lc($opt{'format'}) eq 'billco' ) {
1121 flock(CSV, LOCK_UN);
1126 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1129 open(CSV,">>$file") or die "can't open $file: $!";
1130 flock(CSV, LOCK_EX);
1136 flock(CSV, LOCK_UN);
1143 =item print_csv OPTION => VALUE, ...
1145 Returns CSV data for this invoice.
1149 format - 'default' or 'billco'
1151 Returns a list consisting of two scalars. The first is a single line of CSV
1152 header information for this invoice. The second is one or more lines of CSV
1153 detail information for this invoice.
1155 If I<format> is not specified or "default", the fields of the CSV file are as
1158 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1162 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1164 B<record_type> is C<cust_bill> for the initial header line only. The
1165 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1166 fields are filled in.
1168 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1169 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1172 =item invnum - invoice number
1174 =item custnum - customer number
1176 =item _date - invoice date
1178 =item charged - total invoice amount
1180 =item first - customer first name
1182 =item last - customer first name
1184 =item company - company name
1186 =item address1 - address line 1
1188 =item address2 - address line 1
1198 =item pkg - line item description
1200 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1202 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1204 =item sdate - start date for recurring fee
1206 =item edate - end date for recurring fee
1210 If I<format> is "billco", the fields of the header CSV file are as follows:
1212 +-------------------------------------------------------------------+
1213 | FORMAT HEADER FILE |
1214 |-------------------------------------------------------------------|
1215 | Field | Description | Name | Type | Width |
1216 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1217 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1218 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1219 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1220 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1221 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1222 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1223 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1224 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1225 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1226 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1227 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1228 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1229 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1230 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1231 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1232 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1233 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1234 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1235 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1236 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1237 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1238 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1239 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1240 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1241 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1242 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1243 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1244 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1245 +-------+-------------------------------+------------+------+-------+
1247 If I<format> is "billco", the fields of the detail CSV file are as follows:
1249 FORMAT FOR DETAIL FILE
1251 Field | Description | Name | Type | Width
1252 1 | N/A-Leave Empty | RC | CHAR | 2
1253 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1254 3 | Account Number | TRACCTNUM | CHAR | 15
1255 4 | Invoice Number | TRINVOICE | CHAR | 15
1256 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1257 6 | Transaction Detail | DETAILS | CHAR | 100
1258 7 | Amount | AMT | NUM* | 9
1259 8 | Line Format Control** | LNCTRL | CHAR | 2
1260 9 | Grouping Code | GROUP | CHAR | 2
1261 10 | User Defined | ACCT CODE | CHAR | 15
1266 my($self, %opt) = @_;
1268 eval "use Text::CSV_XS";
1271 my $cust_main = $self->cust_main;
1273 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1275 if ( lc($opt{'format'}) eq 'billco' ) {
1278 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1280 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1282 my( $previous_balance, @unused ) = $self->previous; #previous balance
1284 my $pmt_cr_applied = 0;
1285 $pmt_cr_applied += $_->{'amount'}
1286 foreach ( $self->_items_payments, $self->_items_credits ) ;
1288 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1291 '', # 1 | N/A-Leave Empty CHAR 2
1292 '', # 2 | N/A-Leave Empty CHAR 15
1293 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1294 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1295 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1296 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1297 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1298 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1299 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1300 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1301 '', # 10 | Ancillary Billing Information CHAR 30
1302 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1303 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1306 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1309 $duedate, # 14 | Bill Due Date CHAR 10
1311 $previous_balance, # 15 | Previous Balance NUM* 9
1312 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1313 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1314 $totaldue, # 18 | Total Amt Due NUM* 9
1315 $totaldue, # 19 | Total Amt Due NUM* 9
1316 '', # 20 | 30 Day Aging NUM* 9
1317 '', # 21 | 60 Day Aging NUM* 9
1318 '', # 22 | 90 Day Aging NUM* 9
1319 'N', # 23 | Y/N CHAR 1
1320 '', # 24 | Remittance automation CHAR 100
1321 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1322 $self->custnum, # 26 | Customer Reference Number CHAR 15
1323 '0', # 27 | Federal Tax*** NUM* 9
1324 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1325 '0', # 29 | Other Taxes & Fees*** NUM* 9
1334 time2str("%x", $self->_date),
1335 sprintf("%.2f", $self->charged),
1336 ( map { $cust_main->getfield($_) }
1337 qw( first last company address1 address2 city state zip country ) ),
1339 ) or die "can't create csv";
1342 my $header = $csv->string. "\n";
1345 if ( lc($opt{'format'}) eq 'billco' ) {
1348 foreach my $item ( $self->_items_pkg ) {
1351 '', # 1 | N/A-Leave Empty CHAR 2
1352 '', # 2 | N/A-Leave Empty CHAR 15
1353 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1354 $self->invnum, # 4 | Invoice Number CHAR 15
1355 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1356 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1357 $item->{'amount'}, # 7 | Amount NUM* 9
1358 '', # 8 | Line Format Control** CHAR 2
1359 '', # 9 | Grouping Code CHAR 2
1360 '', # 10 | User Defined CHAR 15
1363 $detail .= $csv->string. "\n";
1369 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1371 my($pkg, $setup, $recur, $sdate, $edate);
1372 if ( $cust_bill_pkg->pkgnum ) {
1374 ($pkg, $setup, $recur, $sdate, $edate) = (
1375 $cust_bill_pkg->part_pkg->pkg,
1376 ( $cust_bill_pkg->setup != 0
1377 ? sprintf("%.2f", $cust_bill_pkg->setup )
1379 ( $cust_bill_pkg->recur != 0
1380 ? sprintf("%.2f", $cust_bill_pkg->recur )
1382 ( $cust_bill_pkg->sdate
1383 ? time2str("%x", $cust_bill_pkg->sdate)
1385 ($cust_bill_pkg->edate
1386 ?time2str("%x", $cust_bill_pkg->edate)
1390 } else { #pkgnum tax
1391 next unless $cust_bill_pkg->setup != 0;
1392 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1393 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1395 ($pkg, $setup, $recur, $sdate, $edate) =
1396 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1402 ( map { '' } (1..11) ),
1403 ($pkg, $setup, $recur, $sdate, $edate)
1404 ) or die "can't create csv";
1406 $detail .= $csv->string. "\n";
1412 ( $header, $detail );
1418 Pays this invoice with a compliemntary payment. If there is an error,
1419 returns the error, otherwise returns false.
1425 my $cust_pay = new FS::cust_pay ( {
1426 'invnum' => $self->invnum,
1427 'paid' => $self->owed,
1430 'payinfo' => $self->cust_main->payinfo,
1438 Attempts to pay this invoice with a credit card payment via a
1439 Business::OnlinePayment realtime gateway. See
1440 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1441 for supported processors.
1447 $self->realtime_bop( 'CC', @_ );
1452 Attempts to pay this invoice with an electronic check (ACH) payment via a
1453 Business::OnlinePayment realtime gateway. See
1454 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1455 for supported processors.
1461 $self->realtime_bop( 'ECHECK', @_ );
1466 Attempts to pay this invoice with phone bill (LEC) payment via a
1467 Business::OnlinePayment realtime gateway. See
1468 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1469 for supported processors.
1475 $self->realtime_bop( 'LEC', @_ );
1479 my( $self, $method ) = @_;
1481 my $cust_main = $self->cust_main;
1482 my $balance = $cust_main->balance;
1483 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1484 $amount = sprintf("%.2f", $amount);
1485 return "not run (balance $balance)" unless $amount > 0;
1487 my $description = 'Internet Services';
1488 if ( $conf->exists('business-onlinepayment-description') ) {
1489 my $dtempl = $conf->config('business-onlinepayment-description');
1491 my $agent_obj = $cust_main->agent
1492 or die "can't retreive agent for $cust_main (agentnum ".
1493 $cust_main->agentnum. ")";
1494 my $agent = $agent_obj->agent;
1495 my $pkgs = join(', ',
1496 map { $_->part_pkg->pkg }
1497 grep { $_->pkgnum } $self->cust_bill_pkg
1499 $description = eval qq("$dtempl");
1502 $cust_main->realtime_bop($method, $amount,
1503 'description' => $description,
1504 'invnum' => $self->invnum,
1509 =item batch_card OPTION => VALUE...
1511 Adds a payment for this invoice to the pending credit card batch (see
1512 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1513 runs the payment using a realtime gateway.
1518 my ($self, %options) = @_;
1519 my $cust_main = $self->cust_main;
1521 $options{invnum} = $self->invnum;
1523 $cust_main->batch_card(%options);
1526 sub _agent_template {
1528 $self->cust_main->agent_template;
1531 sub _agent_invoice_from {
1533 $self->cust_main->agent_invoice_from;
1536 =item print_text [ TIME [ , TEMPLATE ] ]
1538 Returns an text invoice, as a list of lines.
1540 TIME an optional value used to control the printing of overdue messages. The
1541 default is now. It isn't the date of the invoice; that's the `_date' field.
1542 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1543 L<Time::Local> and L<Date::Parse> for conversion functions.
1548 my( $self, $today, $template ) = @_;
1550 my %params = ( 'format' => 'template' );
1551 $params{'time'} = $today if $today;
1552 $params{'template'} = $template if $template;
1554 $self->print_generic( %params );
1557 =item print_latex [ TIME [ , TEMPLATE ] ]
1559 Internal method - returns a filename of a filled-in LaTeX template for this
1560 invoice (Note: add ".tex" to get the actual filename), and a filename of
1561 an associated logo (with the .eps extension included).
1563 See print_ps and print_pdf for methods that return PostScript and PDF output.
1565 TIME an optional value used to control the printing of overdue messages. The
1566 default is now. It isn't the date of the invoice; that's the `_date' field.
1567 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1568 L<Time::Local> and L<Date::Parse> for conversion functions.
1574 my( $self, $today, $template ) = @_;
1576 my %params = ( 'format' => 'latex' );
1577 $params{'time'} = $today if $today;
1578 $params{'template'} = $template if $template;
1580 $template ||= $self->_agent_template;
1582 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1583 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1587 ) or die "can't open temp file: $!\n";
1589 if ($template && $conf->exists("logo_${template}.eps")) {
1590 print $lh $conf->config_binary("logo_${template}.eps")
1591 or die "can't write temp file: $!\n";
1593 print $lh $conf->config_binary('logo.eps')
1594 or die "can't write temp file: $!\n";
1597 $params{'logo_file'} = $lh->filename;
1599 my @filled_in = $self->print_generic( %params );
1601 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1605 ) or die "can't open temp file: $!\n";
1606 print $fh join('', @filled_in );
1609 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1610 return ($1, $params{'logo_file'});
1614 =item print_generic OPTIONS_HASH
1616 Internal method - returns a filled-in template for this invoice as a scalar.
1618 See print_ps and print_pdf for methods that return PostScript and PDF output.
1620 Non optional options include
1621 format - latex, html, template
1623 Optional options include
1625 template - a value used as a suffix for a configuration template
1627 time - a value used to control the printing of overdue messages. The
1628 default is now. It isn't the date of the invoice; that's the `_date' field.
1629 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1630 L<Time::Local> and L<Date::Parse> for conversion functions.
1634 unsquelch_cdr - overrides any per customer cdr squelching when true
1640 my( $self, %params ) = @_;
1641 my $today = $params{today} ? $params{today} : time;
1642 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1645 my $format = $params{format};
1646 die "Unknown format: $format"
1647 unless $format =~ /^(latex|html|template)$/;
1649 my $cust_main = $self->cust_main;
1650 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1651 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1654 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1655 'html' => [ '<%=', '%>' ],
1656 'template' => [ '{', '}' ],
1659 #create the template
1660 my $template = $params{template} ? $params{template} : $self->_agent_template;
1661 my $templatefile = "invoice_$format";
1662 $templatefile .= "_$template"
1663 if length($template);
1664 my @invoice_template = map "$_\n", $conf->config($templatefile)
1665 or die "cannot load config data $templatefile";
1668 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1669 #change this to a die when the old code is removed
1670 warn "old-style invoice template $templatefile; ".
1671 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1672 $old_latex = 'true';
1673 @invoice_template = _translate_old_latex_format(@invoice_template);
1676 my $text_template = new Text::Template(
1678 SOURCE => \@invoice_template,
1679 DELIMITERS => $delimiters{$format},
1682 $text_template->compile()
1683 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1686 # additional substitution could possibly cause breakage in existing templates
1687 my %convert_maps = (
1689 'notes' => sub { map "$_", @_ },
1690 'footer' => sub { map "$_", @_ },
1691 'smallfooter' => sub { map "$_", @_ },
1692 'returnaddress' => sub { map "$_", @_ },
1693 'coupon' => sub { map "$_", @_ },
1699 s/%%(.*)$/<!-- $1 -->/g;
1700 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1701 s/\\begin\{enumerate\}/<ol>/g;
1703 s/\\end\{enumerate\}/<\/ol>/g;
1704 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1713 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1715 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1720 s/\\\\\*?\s*$/<BR>/;
1721 s/\\hyphenation\{[\w\s\-]+}//;
1725 'coupon' => sub { "" },
1732 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1733 s/\\begin\{enumerate\}//g;
1735 s/\\end\{enumerate\}//g;
1736 s/\\textbf\{(.*)\}/$1/g;
1743 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1745 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1750 s/\\\\\*?\s*$/\n/; # dubious
1751 s/\\hyphenation\{[\w\s\-]+}//;
1755 'coupon' => sub { "" },
1760 # hashes for differing output formats
1761 my %nbsps = ( 'latex' => '~',
1762 'html' => '', # '&nbps;' would be nice
1763 'template' => '', # not used
1765 my $nbsp = $nbsps{$format};
1767 my %escape_functions = ( 'latex' => \&_latex_escape,
1768 'html' => \&encode_entities,
1769 'template' => sub { shift },
1771 my $escape_function = $escape_functions{$format};
1773 my %date_formats = ( 'latex' => '%b %o, %Y',
1774 'html' => '%b %o, %Y',
1777 my $date_format = $date_formats{$format};
1779 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1781 'html' => sub { return '<b>'. shift(). '</b>'
1783 'template' => sub { shift },
1785 my $embolden_function = $embolden_functions{$format};
1788 # generate template variables
1791 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1795 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1801 $returnaddress = join("\n",
1802 $conf->config_orbase("invoice_${format}returnaddress", $template)
1805 } elsif ( grep /\S/,
1806 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1808 my $convert_map = $convert_maps{$format}{'returnaddress'};
1811 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1816 } elsif ( grep /\S/, $conf->config('company_address') ) {
1818 my $convert_map = $convert_maps{$format}{'returnaddress'};
1819 $returnaddress = join( "\n", &$convert_map(
1820 map { s/( {2,})/'~' x length($1)/eg;
1824 ( $conf->config('company_name'),
1825 $conf->config('company_address'),
1832 my $warning = "Couldn't find a return address; ".
1833 "do you need to set the company_address configuration value?";
1835 $returnaddress = $nbsp;
1836 #$returnaddress = $warning;
1840 my %invoice_data = (
1841 'company_name' => scalar( $conf->config('company_name') ),
1842 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1843 'custnum' => $self->custnum,
1844 'invnum' => $self->invnum,
1845 'date' => time2str($date_format, $self->_date),
1846 'today' => time2str('%b %o, %Y', $today),
1847 'agent' => &$escape_function($cust_main->agent->agent),
1848 'agent_custid' => &$escape_function($cust_main->agent_custid),
1849 'payname' => &$escape_function($cust_main->payname),
1850 'company' => &$escape_function($cust_main->company),
1851 'address1' => &$escape_function($cust_main->address1),
1852 'address2' => &$escape_function($cust_main->address2),
1853 'city' => &$escape_function($cust_main->city),
1854 'state' => &$escape_function($cust_main->state),
1855 'zip' => &$escape_function($cust_main->zip),
1856 'returnaddress' => $returnaddress,
1858 'terms' => $self->terms,
1859 'template' => $params{'template'},
1860 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1861 # better hang on to conf_dir for a while
1862 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1865 'current_charges' => sprintf("%.2f", $self->charged),
1866 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1867 'ship_enable' => $conf->exists('invoice-ship_address'),
1868 'unitprices' => $conf->exists('invoice-unitprice'),
1871 my $countrydefault = $conf->config('countrydefault') || 'US';
1872 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1873 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1874 my $method = $prefix.$_;
1875 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1877 $invoice_data{'ship_country'} = ''
1878 if ( $invoice_data{'ship_country'} eq $countrydefault );
1880 $invoice_data{'cid'} = $params{'cid'}
1883 if ( $cust_main->country eq $countrydefault ) {
1884 $invoice_data{'country'} = '';
1886 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1890 $invoice_data{'address'} = \@address;
1892 $cust_main->payname.
1893 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1894 ? " (P.O. #". $cust_main->payinfo. ")"
1898 push @address, $cust_main->company
1899 if $cust_main->company;
1900 push @address, $cust_main->address1;
1901 push @address, $cust_main->address2
1902 if $cust_main->address2;
1904 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1905 push @address, $invoice_data{'country'}
1906 if $invoice_data{'country'};
1908 while (scalar(@address) < 5);
1910 $invoice_data{'logo_file'} = $params{'logo_file'}
1911 if $params{'logo_file'};
1913 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1914 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1915 #my $balance_due = $self->owed + $pr_total - $cr_total;
1916 my $balance_due = $self->owed + $pr_total;
1917 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1918 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1920 #do variable substitution in notes, footer, smallfooter
1921 foreach my $include (qw( notes footer smallfooter coupon )) {
1923 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1926 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1928 @inc_src = $conf->config($inc_file);
1932 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1934 my $convert_map = $convert_maps{$format}{$include};
1936 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1937 s/--\@\]/$delimiters{$format}[1]/g;
1940 &$convert_map( $conf->config($inc_file) );
1944 my $inc_tt = new Text::Template (
1946 SOURCE => [ map "$_\n", @inc_src ],
1947 DELIMITERS => $delimiters{$format},
1948 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1950 unless ( $inc_tt->compile() ) {
1951 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1952 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1956 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1958 $invoice_data{$include} =~ s/\n+$//
1959 if ($format eq 'latex');
1962 $invoice_data{'po_line'} =
1963 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1964 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1967 my %money_chars = ( 'latex' => '',
1968 'html' => $conf->config('money_char') || '$',
1971 my $money_char = $money_chars{$format};
1973 my %other_money_chars = ( 'latex' => '\dollar ',
1974 'html' => $conf->config('money_char') || '$',
1977 my $other_money_char = $other_money_chars{$format};
1979 my @detail_items = ();
1980 my @total_items = ();
1984 $invoice_data{'detail_items'} = \@detail_items;
1985 $invoice_data{'total_items'} = \@total_items;
1986 $invoice_data{'buf'} = \@buf;
1987 $invoice_data{'sections'} = \@sections;
1989 my $previous_section = { 'description' => 'Previous Charges',
1990 'subtotal' => $other_money_char.
1991 sprintf('%.2f', $pr_total),
1995 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1996 'subtotal' => $taxtotal }; # adjusted below
1998 my $adjusttotal = 0;
1999 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2000 'subtotal' => 0 }; # adjusted below
2002 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2003 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2004 my $late_sections = [];
2005 if ( $multisection ) {
2006 push @sections, $self->_items_sections( $late_sections );
2008 push @sections, { 'description' => '', 'subtotal' => '' };
2011 foreach my $line_item ( $conf->exists('disable_previous_balance')
2013 : $self->_items_previous
2017 ext_description => [],
2019 $detail->{'ref'} = $line_item->{'pkgnum'};
2020 $detail->{'quantity'} = 1;
2021 $detail->{'section'} = $previous_section;
2022 $detail->{'description'} = &$escape_function($line_item->{'description'});
2023 if ( exists $line_item->{'ext_description'} ) {
2024 @{$detail->{'ext_description'}} = map {
2025 &$escape_function($_);
2026 } @{$line_item->{'ext_description'}};
2028 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2029 $line_item->{'amount'};
2030 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2032 push @detail_items, $detail;
2033 push @buf, [ $detail->{'description'},
2034 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2038 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2039 push @buf, ['','-----------'];
2040 push @buf, [ 'Total Previous Balance',
2041 $money_char. sprintf("%10.2f", $pr_total) ];
2045 foreach my $section (@sections, @$late_sections) {
2047 $section->{'subtotal'} = $other_money_char.
2048 sprintf('%.2f', $section->{'subtotal'})
2051 if ( $section->{'description'} ) {
2052 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2058 $options{'section'} = $section if $multisection;
2059 $options{'format'} = $format;
2060 $options{'escape_function'} = $escape_function;
2061 $options{'format_function'} = sub { () } unless $unsquelched;
2062 $options{'unsquelched'} = $unsquelched;
2064 foreach my $line_item ( $self->_items_pkg(%options) ) {
2066 ext_description => [],
2068 $detail->{'ref'} = $line_item->{'pkgnum'};
2069 $detail->{'quantity'} = $line_item->{'quantity'};
2070 $detail->{'section'} = $section;
2071 $detail->{'description'} = &$escape_function($line_item->{'description'});
2072 if ( exists $line_item->{'ext_description'} ) {
2073 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2075 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2076 $line_item->{'amount'};
2077 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2078 $line_item->{'unit_amount'};
2079 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2081 push @detail_items, $detail;
2082 push @buf, ( [ $detail->{'description'},
2083 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2085 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2089 if ( $section->{'description'} ) {
2090 push @buf, ( ['','-----------'],
2091 [ $section->{'description'}. ' sub-total',
2092 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2101 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2102 unshift @sections, $previous_section if $pr_total;
2105 foreach my $tax ( $self->_items_tax ) {
2107 $total->{'total_item'} = &$escape_function($tax->{'description'});
2108 $taxtotal += $tax->{'amount'};
2109 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2110 if ( $multisection ) {
2111 my $money = $old_latex ? '' : $money_char;
2112 push @detail_items, {
2113 ext_description => [],
2116 description => &$escape_function($tax->{'description'}),
2117 amount => $money. $tax->{'amount'},
2119 section => $tax_section,
2122 push @total_items, $total;
2124 push @buf,[ $total->{'total_item'},
2125 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2132 $total->{'total_item'} = 'Sub-total';
2133 $total->{'total_amount'} =
2134 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2136 if ( $multisection ) {
2137 $tax_section->{'subtotal'} = $other_money_char.
2138 sprintf('%.2f', $taxtotal);
2139 $tax_section->{'pretotal'} = 'New charges sub-total '.
2140 $total->{'total_amount'};
2141 push @sections, $tax_section if $taxtotal;
2143 unshift @total_items, $total;
2146 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2148 push @buf,['','-----------'];
2149 push @buf,[( $conf->exists('disable_previous_balance')
2151 : 'Total New Charges'
2153 $money_char. sprintf("%10.2f",$self->charged) ];
2158 $total->{'total_item'} = &$embolden_function('Total');
2159 $total->{'total_amount'} =
2160 &$embolden_function(
2163 $self->charged + ( $conf->exists('disable_previous_balance')
2169 if ( $multisection ) {
2170 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2171 sprintf('%.2f', $self->charged );
2173 push @total_items, $total;
2175 push @buf,['','-----------'];
2176 push @buf,['Total Charges',
2178 sprintf( '%10.2f', $self->charged +
2179 ( $conf->exists('disable_previous_balance')
2188 unless ( $conf->exists('disable_previous_balance') ) {
2189 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2192 my $credittotal = 0;
2193 foreach my $credit ( $self->_items_credits ) {
2195 $total->{'total_item'} = &$escape_function($credit->{'description'});
2196 $credittotal += $credit->{'amount'};
2197 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2198 $adjusttotal += $credit->{'amount'};
2199 if ( $multisection ) {
2200 my $money = $old_latex ? '' : $money_char;
2201 push @detail_items, {
2202 ext_description => [],
2205 description => &$escape_function($credit->{'description'}),
2206 amount => $money. $credit->{'amount'},
2208 section => $adjust_section,
2211 push @total_items, $total;
2214 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2217 foreach ( $self->cust_credited ) {
2219 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2221 my $reason = substr($_->cust_credit->reason,0,32);
2222 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2223 $reason = " ($reason) " if $reason;
2225 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2226 $money_char. sprintf("%10.2f",$_->amount)
2231 my $paymenttotal = 0;
2232 foreach my $payment ( $self->_items_payments ) {
2234 $total->{'total_item'} = &$escape_function($payment->{'description'});
2235 $paymenttotal += $payment->{'amount'};
2236 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2237 $adjusttotal += $payment->{'amount'};
2238 if ( $multisection ) {
2239 my $money = $old_latex ? '' : $money_char;
2240 push @detail_items, {
2241 ext_description => [],
2244 description => &$escape_function($payment->{'description'}),
2245 amount => $money. $payment->{'amount'},
2247 section => $adjust_section,
2250 push @total_items, $total;
2252 push @buf, [ $payment->{'description'},
2253 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2256 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2258 if ( $multisection ) {
2259 $adjust_section->{'subtotal'} = $other_money_char.
2260 sprintf('%.2f', $adjusttotal);
2261 push @sections, $adjust_section;
2266 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2267 $total->{'total_amount'} =
2268 &$embolden_function(
2269 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2271 if ( $multisection ) {
2272 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2273 $total->{'total_amount'};
2275 push @total_items, $total;
2277 push @buf,['','-----------'];
2278 push @buf,[$self->balance_due_msg, $money_char.
2279 sprintf("%10.2f", $balance_due ) ];
2283 if ( $multisection ) {
2284 push @sections, @$late_sections
2290 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2291 /invoice_lines\((\d*)\)/;
2292 $invoice_lines += $1 || scalar(@buf);
2295 die "no invoice_lines() functions in template?"
2296 if ( $format eq 'template' && !$wasfunc );
2298 if ($format eq 'template') {
2300 if ( $invoice_lines ) {
2301 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2302 $invoice_data{'total_pages'}++
2303 if scalar(@buf) % $invoice_lines;
2306 #setup subroutine for the template
2307 sub FS::cust_bill::_template::invoice_lines {
2308 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2310 scalar(@FS::cust_bill::_template::buf)
2311 ? shift @FS::cust_bill::_template::buf
2320 push @collect, split("\n",
2321 $text_template->fill_in( HASH => \%invoice_data,
2322 PACKAGE => 'FS::cust_bill::_template'
2325 $FS::cust_bill::_template::page++;
2327 map "$_\n", @collect;
2329 warn "filling in template for invoice ". $self->invnum. "\n"
2331 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2334 $text_template->fill_in(HASH => \%invoice_data);
2338 =item print_ps [ TIME [ , TEMPLATE ] ]
2340 Returns an postscript invoice, as a scalar.
2342 TIME an optional value used to control the printing of overdue messages. The
2343 default is now. It isn't the date of the invoice; that's the `_date' field.
2344 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2345 L<Time::Local> and L<Date::Parse> for conversion functions.
2352 my ($file, $lfile) = $self->print_latex(@_);
2353 my $ps = generate_ps($file);
2359 =item print_pdf [ TIME [ , TEMPLATE ] ]
2361 Returns an PDF invoice, as a scalar.
2363 TIME an optional value used to control the printing of overdue messages. The
2364 default is now. It isn't the date of the invoice; that's the `_date' field.
2365 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2366 L<Time::Local> and L<Date::Parse> for conversion functions.
2373 my ($file, $lfile) = $self->print_latex(@_);
2374 my $pdf = generate_pdf($file);
2380 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2382 Returns an HTML invoice, as a scalar.
2384 TIME an optional value used to control the printing of overdue messages. The
2385 default is now. It isn't the date of the invoice; that's the `_date' field.
2386 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2387 L<Time::Local> and L<Date::Parse> for conversion functions.
2389 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2390 when emailing the invoice as part of a multipart/related MIME email.
2395 my( $self, $today, $template, $cid ) = @_;
2397 my %params = ( 'format' => 'html' );
2398 $params{'time'} = $today if $today;
2399 $params{'template'} = $template if $template;
2400 $params{'cid'} = $cid if $cid;
2402 $self->print_generic( %params );
2405 # quick subroutine for print_latex
2407 # There are ten characters that LaTeX treats as special characters, which
2408 # means that they do not simply typeset themselves:
2409 # # $ % & ~ _ ^ \ { }
2411 # TeX ignores blanks following an escaped character; if you want a blank (as
2412 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2416 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2417 $value =~ s/([<>])/\$$1\$/g;
2421 #utility methods for print_*
2423 sub _translate_old_latex_format {
2424 warn "_translate_old_latex_format called\n"
2431 if ( $line =~ /^%%Detail\s*$/ ) {
2433 push @template, q![@--!,
2434 q! foreach my $_tr_line (@detail_items) {!,
2435 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2436 q! $_tr_line->{'description'} .= !,
2437 q! "\\tabularnewline\n~~".!,
2438 q! join( "\\tabularnewline\n~~",!,
2439 q! @{$_tr_line->{'ext_description'}}!,
2443 while ( ( my $line_item_line = shift )
2444 !~ /^%%EndDetail\s*$/ ) {
2445 $line_item_line =~ s/'/\\'/g; # nice LTS
2446 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2447 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2448 push @template, " \$OUT .= '$line_item_line';";
2451 push @template, '}',
2454 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2456 push @template, '[@--',
2457 ' foreach my $_tr_line (@total_items) {';
2459 while ( ( my $total_item_line = shift )
2460 !~ /^%%EndTotalDetails\s*$/ ) {
2461 $total_item_line =~ s/'/\\'/g; # nice LTS
2462 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2463 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2464 push @template, " \$OUT .= '$total_item_line';";
2467 push @template, '}',
2471 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2472 push @template, $line;
2478 warn "$_\n" foreach @template;
2487 #check for an invoice- specific override (eventually)
2489 #check for a customer- specific override
2490 return $self->cust_main->invoice_terms
2491 if $self->cust_main->invoice_terms;
2493 #use configured default or default default
2494 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2500 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2501 $duedate = $self->_date() + ( $1 * 86400 );
2508 $self->due_date ? time2str(shift, $self->due_date) : '';
2511 sub balance_due_msg {
2513 my $msg = 'Balance Due';
2514 return $msg unless $self->terms;
2515 if ( $self->due_date ) {
2516 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2517 } elsif ( $self->terms ) {
2518 $msg .= ' - '. $self->terms;
2523 sub balance_due_date {
2526 if ( $conf->exists('invoice_default_terms')
2527 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2528 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2533 =item invnum_date_pretty
2535 Returns a string with the invoice number and date, for example:
2536 "Invoice #54 (3/20/2008)"
2540 sub invnum_date_pretty {
2542 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2545 sub _items_sections {
2552 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2555 if ( $cust_bill_pkg->pkgnum > 0 ) {
2557 my $desc = $cust_bill_pkg->section;
2559 if ( $cust_bill_pkg->post_total ) {
2560 $l{$desc} += $cust_bill_pkg->setup
2561 if ( $cust_bill_pkg->setup != 0 );
2563 $l{$desc} += $cust_bill_pkg->recur
2564 if ( $cust_bill_pkg->recur != 0 );
2567 $s{$desc} += $cust_bill_pkg->setup
2568 if ( $cust_bill_pkg->setup != 0 );
2570 $s{$desc} += $cust_bill_pkg->recur
2571 if ( $cust_bill_pkg->recur != 0 );
2578 push @$late, map { { 'description' => $_,
2579 'subtotal' => $l{$_},
2583 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2590 #my @display = scalar(@_)
2592 # : qw( _items_previous _items_pkg );
2593 # #: qw( _items_pkg );
2594 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2595 my @display = qw( _items_previous _items_pkg );
2598 foreach my $display ( @display ) {
2599 push @b, $self->$display(@_);
2604 sub _items_previous {
2606 my $cust_main = $self->cust_main;
2607 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2609 foreach ( @pr_cust_bill ) {
2611 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2612 ' ('. time2str('%x',$_->_date). ')',
2613 #'pkgpart' => 'N/A',
2615 'amount' => sprintf("%.2f", $_->owed),
2621 # 'description' => 'Previous Balance',
2622 # #'pkgpart' => 'N/A',
2623 # 'pkgnum' => 'N/A',
2624 # 'amount' => sprintf("%10.2f", $pr_total ),
2625 # 'ext_description' => [ map {
2626 # "Invoice ". $_->invnum.
2627 # " (". time2str("%x",$_->_date). ") ".
2628 # sprintf("%10.2f", $_->owed)
2629 # } @pr_cust_bill ],
2637 my $section = delete $options{'section'};
2639 grep { $_->pkgnum &&
2641 ? $_->section eq $section->{'description'}
2644 } $self->cust_bill_pkg;
2645 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2649 return 0 unless $a cmp $b;
2650 return -1 if $b eq 'Tax';
2651 return 1 if $a eq 'Tax';
2652 return -1 if $b eq 'Other surcharges';
2653 return 1 if $a eq 'Other surcharges';
2659 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2660 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2663 sub _items_cust_bill_pkg {
2665 my $cust_bill_pkg = shift;
2668 my $format = $opt{format} || '';
2669 my $escape_function = $opt{escape_function} || sub { shift };
2670 my $format_function = $opt{format_function} || '';
2671 my $unsquelched = $opt{unsquelched} || '';
2674 foreach my $cust_bill_pkg ( grep { $unsquelched ? 1 : ! $_->separate_cdr }
2679 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2681 my $desc = $cust_bill_pkg->desc;
2683 my %details_opt = ( 'format' => $format,
2684 'escape_function' => $escape_function,
2685 'format_function' => $format_function,
2688 if ( $cust_bill_pkg->pkgnum > 0 ) {
2690 if ( $cust_bill_pkg->setup != 0 ) {
2692 my $description = $desc;
2693 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2695 my @d = map &{$escape_function}($_),
2696 $cust_pkg->h_labels_short($self->_date);
2697 push @d, $cust_bill_pkg->details(%details_opt)
2698 if $cust_bill_pkg->recur == 0;
2701 description => $description,
2702 #pkgpart => $part_pkg->pkgpart,
2703 pkgnum => $cust_bill_pkg->pkgnum,
2704 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2705 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2706 quantity => $cust_bill_pkg->quantity,
2707 ext_description => \@d,
2711 if ( $cust_bill_pkg->recur != 0 ) {
2713 my $description = $desc;
2714 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2715 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2716 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2719 #at least until cust_bill_pkg has "past" ranges in addition to
2720 #the "future" sdate/edate ones... see #3032
2721 my @d = map &{$escape_function}($_),
2722 $cust_pkg->h_labels_short($self->_date);
2723 #$cust_bill_pkg->edate,
2724 #$cust_bill_pkg->sdate),
2725 @d = () if $cust_bill_pkg->itemdesc;
2726 push @d, $cust_bill_pkg->details(%details_opt);
2729 description => $description,
2730 #pkgpart => $part_pkg->pkgpart,
2731 pkgnum => $cust_bill_pkg->pkgnum,
2732 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2733 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2734 quantity => $cust_bill_pkg->quantity,
2735 ext_description => \@d,
2740 } else { #pkgnum tax or one-shot line item (??)
2742 if ( $cust_bill_pkg->setup != 0 ) {
2744 'description' => $desc,
2745 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2748 if ( $cust_bill_pkg->recur != 0 ) {
2750 'description' => "$desc (".
2751 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2752 time2str("%x", $cust_bill_pkg->edate). ')',
2753 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2765 sub _items_credits {
2770 foreach ( $self->cust_credited ) {
2772 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2774 my $reason = $_->cust_credit->reason;
2775 #my $reason = substr($_->cust_credit->reason,0,32);
2776 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2777 $reason = " ($reason) " if $reason;
2779 #'description' => 'Credit ref\#'. $_->crednum.
2780 # " (". time2str("%x",$_->cust_credit->_date) .")".
2782 'description' => 'Credit applied '.
2783 time2str("%x",$_->cust_credit->_date). $reason,
2784 'amount' => sprintf("%.2f",$_->amount),
2787 #foreach ( @cr_cust_credit ) {
2789 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2790 # $money_char. sprintf("%10.2f",$_->credited)
2798 sub _items_payments {
2802 #get & print payments
2803 foreach ( $self->cust_bill_pay ) {
2805 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2808 'description' => "Payment received ".
2809 time2str("%x",$_->cust_pay->_date ),
2810 'amount' => sprintf("%.2f", $_->amount )
2829 sub process_reprint {
2830 process_re_X('print', @_);
2837 sub process_reemail {
2838 process_re_X('email', @_);
2846 process_re_X('fax', @_);
2849 use Storable qw(thaw);
2853 my( $method, $job ) = ( shift, shift );
2854 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2856 my $param = thaw(decode_base64(shift));
2857 warn Dumper($param) if $DEBUG;
2868 my($method, $job, %param ) = @_;
2870 warn "re_X $method for job $job with param:\n".
2871 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2874 #some false laziness w/search/cust_bill.html
2876 my $orderby = 'ORDER BY cust_bill._date';
2878 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2880 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2882 my @cust_bill = qsearch( {
2883 #'select' => "cust_bill.*",
2884 'table' => 'cust_bill',
2885 'addl_from' => $addl_from,
2887 'extra_sql' => $extra_sql,
2888 'order_by' => $orderby,
2892 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2895 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2896 foreach my $cust_bill ( @cust_bill ) {
2897 $cust_bill->$method();
2899 if ( $job ) { #progressbar foo
2901 if ( time - $min_sec > $last ) {
2902 my $error = $job->update_statustext(
2903 int( 100 * $num / scalar(@cust_bill) )
2905 die $error if $error;
2916 =head1 CLASS METHODS
2922 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2928 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2933 Returns an SQL fragment to retreive the net amount (charged minus credited).
2939 'charged - '. $class->credited_sql;
2944 Returns an SQL fragment to retreive the amount paid against this invoice.
2950 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2951 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2956 Returns an SQL fragment to retreive the amount credited against this invoice.
2962 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2963 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2966 =item search_sql HASHREF
2968 Class method which returns an SQL WHERE fragment to search for parameters
2969 specified in HASHREF. Valid parameters are
2975 Epoch date (UNIX timestamp) setting a lower bound for _date values
2979 Epoch date (UNIX timestamp) setting an upper bound for _date values
2993 =item newest_percust
2997 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3002 my($class, $param) = @_;
3004 warn "$me search_sql called with params: \n".
3005 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3010 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3011 push @search, "cust_bill._date >= $1";
3013 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3014 push @search, "cust_bill._date < $1";
3016 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3017 push @search, "cust_bill.invnum >= $1";
3019 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3020 push @search, "cust_bill.invnum <= $1";
3022 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3023 push @search, "cust_main.agentnum = $1";
3026 push @search, '0 != '. FS::cust_bill->owed_sql
3027 if $param->{'open'};
3029 push @search, '0 != '. FS::cust_bill->net_sql
3032 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3033 if $param->{'days'};
3035 if ( $param->{'newest_percust'} ) {
3037 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3038 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3040 my @newest_where = map { my $x = $_;
3041 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3044 grep ! /^cust_main./, @search;
3045 my $newest_where = scalar(@newest_where)
3046 ? ' AND '. join(' AND ', @newest_where)
3050 push @search, "cust_bill._date = (
3051 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3052 WHERE newest_cust_bill.custnum = cust_bill.custnum
3058 my $curuser = $FS::CurrentUser::CurrentUser;
3059 if ( $curuser->username eq 'fs_queue'
3060 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3062 my $newuser = qsearchs('access_user', {
3063 'username' => $username,
3067 $curuser = $newuser;
3069 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3073 push @search, $curuser->agentnums_sql;
3075 join(' AND ', @search );
3087 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3088 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base