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 ftp_invoice [ TEMPLATENAME ]
979 Sends this invoice data via FTP.
981 TEMPLATENAME is unused?
987 my $template = scalar(@_) ? shift : '';
991 'server' => $conf->config('cust_bill-ftpserver'),
992 'username' => $conf->config('cust_bill-ftpusername'),
993 'password' => $conf->config('cust_bill-ftppassword'),
994 'dir' => $conf->config('cust_bill-ftpdir'),
995 'format' => $conf->config('cust_bill-ftpformat'),
999 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1001 Like B<send>, but only sends the invoice if it is the newest open invoice for
1006 sub send_if_newest {
1011 grep { $_->owed > 0 }
1012 qsearch('cust_bill', {
1013 'custnum' => $self->custnum,
1014 #'_date' => { op=>'>', value=>$self->_date },
1015 'invnum' => { op=>'>', value=>$self->invnum },
1022 =item send_csv OPTION => VALUE, ...
1024 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1028 protocol - currently only "ftp"
1034 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1035 and YYMMDDHHMMSS is a timestamp.
1037 See L</print_csv> for a description of the output format.
1042 my($self, %opt) = @_;
1046 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1047 mkdir $spooldir, 0700 unless -d $spooldir;
1049 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1050 my $file = "$spooldir/$tracctnum.csv";
1052 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1054 open(CSV, ">$file") or die "can't open $file: $!";
1062 if ( $opt{protocol} eq 'ftp' ) {
1063 eval "use Net::FTP;";
1065 $net = Net::FTP->new($opt{server}) or die @$;
1067 die "unknown protocol: $opt{protocol}";
1070 $net->login( $opt{username}, $opt{password} )
1071 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1073 $net->binary or die "can't set binary mode";
1075 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1077 $net->put($file) or die "can't put $file: $!";
1087 Spools CSV invoice data.
1093 =item format - 'default' or 'billco'
1095 =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>).
1097 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1099 =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.
1106 my($self, %opt) = @_;
1108 my $cust_main = $self->cust_main;
1110 if ( $opt{'dest'} ) {
1111 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1112 $cust_main->invoicing_list;
1113 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1114 || ! keys %invoicing_list;
1117 if ( $opt{'balanceover'} ) {
1119 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1122 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1123 mkdir $spooldir, 0700 unless -d $spooldir;
1125 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1129 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1130 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1133 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1135 open(CSV, ">>$file") or die "can't open $file: $!";
1136 flock(CSV, LOCK_EX);
1141 if ( lc($opt{'format'}) eq 'billco' ) {
1143 flock(CSV, LOCK_UN);
1148 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1151 open(CSV,">>$file") or die "can't open $file: $!";
1152 flock(CSV, LOCK_EX);
1158 flock(CSV, LOCK_UN);
1165 =item print_csv OPTION => VALUE, ...
1167 Returns CSV data for this invoice.
1171 format - 'default' or 'billco'
1173 Returns a list consisting of two scalars. The first is a single line of CSV
1174 header information for this invoice. The second is one or more lines of CSV
1175 detail information for this invoice.
1177 If I<format> is not specified or "default", the fields of the CSV file are as
1180 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1184 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1186 B<record_type> is C<cust_bill> for the initial header line only. The
1187 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1188 fields are filled in.
1190 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1191 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1194 =item invnum - invoice number
1196 =item custnum - customer number
1198 =item _date - invoice date
1200 =item charged - total invoice amount
1202 =item first - customer first name
1204 =item last - customer first name
1206 =item company - company name
1208 =item address1 - address line 1
1210 =item address2 - address line 1
1220 =item pkg - line item description
1222 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1224 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1226 =item sdate - start date for recurring fee
1228 =item edate - end date for recurring fee
1232 If I<format> is "billco", the fields of the header CSV file are as follows:
1234 +-------------------------------------------------------------------+
1235 | FORMAT HEADER FILE |
1236 |-------------------------------------------------------------------|
1237 | Field | Description | Name | Type | Width |
1238 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1239 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1240 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1241 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1242 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1243 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1244 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1245 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1246 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1247 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1248 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1249 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1250 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1251 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1252 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1253 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1254 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1255 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1256 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1257 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1258 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1259 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1260 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1261 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1262 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1263 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1264 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1265 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1266 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1267 +-------+-------------------------------+------------+------+-------+
1269 If I<format> is "billco", the fields of the detail CSV file are as follows:
1271 FORMAT FOR DETAIL FILE
1273 Field | Description | Name | Type | Width
1274 1 | N/A-Leave Empty | RC | CHAR | 2
1275 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1276 3 | Account Number | TRACCTNUM | CHAR | 15
1277 4 | Invoice Number | TRINVOICE | CHAR | 15
1278 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1279 6 | Transaction Detail | DETAILS | CHAR | 100
1280 7 | Amount | AMT | NUM* | 9
1281 8 | Line Format Control** | LNCTRL | CHAR | 2
1282 9 | Grouping Code | GROUP | CHAR | 2
1283 10 | User Defined | ACCT CODE | CHAR | 15
1288 my($self, %opt) = @_;
1290 eval "use Text::CSV_XS";
1293 my $cust_main = $self->cust_main;
1295 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1297 if ( lc($opt{'format'}) eq 'billco' ) {
1300 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1302 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1304 my( $previous_balance, @unused ) = $self->previous; #previous balance
1306 my $pmt_cr_applied = 0;
1307 $pmt_cr_applied += $_->{'amount'}
1308 foreach ( $self->_items_payments, $self->_items_credits ) ;
1310 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1313 '', # 1 | N/A-Leave Empty CHAR 2
1314 '', # 2 | N/A-Leave Empty CHAR 15
1315 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1316 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1317 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1318 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1319 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1320 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1321 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1322 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1323 '', # 10 | Ancillary Billing Information CHAR 30
1324 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1325 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1328 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1331 $duedate, # 14 | Bill Due Date CHAR 10
1333 $previous_balance, # 15 | Previous Balance NUM* 9
1334 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1335 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1336 $totaldue, # 18 | Total Amt Due NUM* 9
1337 $totaldue, # 19 | Total Amt Due NUM* 9
1338 '', # 20 | 30 Day Aging NUM* 9
1339 '', # 21 | 60 Day Aging NUM* 9
1340 '', # 22 | 90 Day Aging NUM* 9
1341 'N', # 23 | Y/N CHAR 1
1342 '', # 24 | Remittance automation CHAR 100
1343 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1344 $self->custnum, # 26 | Customer Reference Number CHAR 15
1345 '0', # 27 | Federal Tax*** NUM* 9
1346 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1347 '0', # 29 | Other Taxes & Fees*** NUM* 9
1356 time2str("%x", $self->_date),
1357 sprintf("%.2f", $self->charged),
1358 ( map { $cust_main->getfield($_) }
1359 qw( first last company address1 address2 city state zip country ) ),
1361 ) or die "can't create csv";
1364 my $header = $csv->string. "\n";
1367 if ( lc($opt{'format'}) eq 'billco' ) {
1370 foreach my $item ( $self->_items_pkg ) {
1373 '', # 1 | N/A-Leave Empty CHAR 2
1374 '', # 2 | N/A-Leave Empty CHAR 15
1375 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1376 $self->invnum, # 4 | Invoice Number CHAR 15
1377 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1378 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1379 $item->{'amount'}, # 7 | Amount NUM* 9
1380 '', # 8 | Line Format Control** CHAR 2
1381 '', # 9 | Grouping Code CHAR 2
1382 '', # 10 | User Defined CHAR 15
1385 $detail .= $csv->string. "\n";
1391 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1393 my($pkg, $setup, $recur, $sdate, $edate);
1394 if ( $cust_bill_pkg->pkgnum ) {
1396 ($pkg, $setup, $recur, $sdate, $edate) = (
1397 $cust_bill_pkg->part_pkg->pkg,
1398 ( $cust_bill_pkg->setup != 0
1399 ? sprintf("%.2f", $cust_bill_pkg->setup )
1401 ( $cust_bill_pkg->recur != 0
1402 ? sprintf("%.2f", $cust_bill_pkg->recur )
1404 ( $cust_bill_pkg->sdate
1405 ? time2str("%x", $cust_bill_pkg->sdate)
1407 ($cust_bill_pkg->edate
1408 ?time2str("%x", $cust_bill_pkg->edate)
1412 } else { #pkgnum tax
1413 next unless $cust_bill_pkg->setup != 0;
1414 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1415 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1417 ($pkg, $setup, $recur, $sdate, $edate) =
1418 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1424 ( map { '' } (1..11) ),
1425 ($pkg, $setup, $recur, $sdate, $edate)
1426 ) or die "can't create csv";
1428 $detail .= $csv->string. "\n";
1434 ( $header, $detail );
1440 Pays this invoice with a compliemntary payment. If there is an error,
1441 returns the error, otherwise returns false.
1447 my $cust_pay = new FS::cust_pay ( {
1448 'invnum' => $self->invnum,
1449 'paid' => $self->owed,
1452 'payinfo' => $self->cust_main->payinfo,
1460 Attempts to pay this invoice with a credit card payment via a
1461 Business::OnlinePayment realtime gateway. See
1462 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1463 for supported processors.
1469 $self->realtime_bop( 'CC', @_ );
1474 Attempts to pay this invoice with an electronic check (ACH) payment via a
1475 Business::OnlinePayment realtime gateway. See
1476 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1477 for supported processors.
1483 $self->realtime_bop( 'ECHECK', @_ );
1488 Attempts to pay this invoice with phone bill (LEC) payment via a
1489 Business::OnlinePayment realtime gateway. See
1490 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1491 for supported processors.
1497 $self->realtime_bop( 'LEC', @_ );
1501 my( $self, $method ) = @_;
1503 my $cust_main = $self->cust_main;
1504 my $balance = $cust_main->balance;
1505 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1506 $amount = sprintf("%.2f", $amount);
1507 return "not run (balance $balance)" unless $amount > 0;
1509 my $description = 'Internet Services';
1510 if ( $conf->exists('business-onlinepayment-description') ) {
1511 my $dtempl = $conf->config('business-onlinepayment-description');
1513 my $agent_obj = $cust_main->agent
1514 or die "can't retreive agent for $cust_main (agentnum ".
1515 $cust_main->agentnum. ")";
1516 my $agent = $agent_obj->agent;
1517 my $pkgs = join(', ',
1518 map { $_->part_pkg->pkg }
1519 grep { $_->pkgnum } $self->cust_bill_pkg
1521 $description = eval qq("$dtempl");
1524 $cust_main->realtime_bop($method, $amount,
1525 'description' => $description,
1526 'invnum' => $self->invnum,
1531 =item batch_card OPTION => VALUE...
1533 Adds a payment for this invoice to the pending credit card batch (see
1534 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1535 runs the payment using a realtime gateway.
1540 my ($self, %options) = @_;
1541 my $cust_main = $self->cust_main;
1543 $options{invnum} = $self->invnum;
1545 $cust_main->batch_card(%options);
1548 sub _agent_template {
1550 $self->cust_main->agent_template;
1553 sub _agent_invoice_from {
1555 $self->cust_main->agent_invoice_from;
1558 =item print_text [ TIME [ , TEMPLATE ] ]
1560 Returns an text invoice, as a list of lines.
1562 TIME an optional value used to control the printing of overdue messages. The
1563 default is now. It isn't the date of the invoice; that's the `_date' field.
1564 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1565 L<Time::Local> and L<Date::Parse> for conversion functions.
1570 my( $self, $today, $template ) = @_;
1572 my %params = ( 'format' => 'template' );
1573 $params{'time'} = $today if $today;
1574 $params{'template'} = $template if $template;
1576 $self->print_generic( %params );
1579 =item print_latex [ TIME [ , TEMPLATE ] ]
1581 Internal method - returns a filename of a filled-in LaTeX template for this
1582 invoice (Note: add ".tex" to get the actual filename), and a filename of
1583 an associated logo (with the .eps extension included).
1585 See print_ps and print_pdf for methods that return PostScript and PDF output.
1587 TIME an optional value used to control the printing of overdue messages. The
1588 default is now. It isn't the date of the invoice; that's the `_date' field.
1589 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1590 L<Time::Local> and L<Date::Parse> for conversion functions.
1596 my( $self, $today, $template ) = @_;
1598 my %params = ( 'format' => 'latex' );
1599 $params{'time'} = $today if $today;
1600 $params{'template'} = $template if $template;
1602 $template ||= $self->_agent_template;
1604 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1605 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1609 ) or die "can't open temp file: $!\n";
1611 if ($template && $conf->exists("logo_${template}.eps")) {
1612 print $lh $conf->config_binary("logo_${template}.eps")
1613 or die "can't write temp file: $!\n";
1615 print $lh $conf->config_binary('logo.eps')
1616 or die "can't write temp file: $!\n";
1619 $params{'logo_file'} = $lh->filename;
1621 my @filled_in = $self->print_generic( %params );
1623 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1627 ) or die "can't open temp file: $!\n";
1628 print $fh join('', @filled_in );
1631 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1632 return ($1, $params{'logo_file'});
1636 =item print_generic OPTIONS_HASH
1638 Internal method - returns a filled-in template for this invoice as a scalar.
1640 See print_ps and print_pdf for methods that return PostScript and PDF output.
1642 Non optional options include
1643 format - latex, html, template
1645 Optional options include
1647 template - a value used as a suffix for a configuration template
1649 time - a value used to control the printing of overdue messages. The
1650 default is now. It isn't the date of the invoice; that's the `_date' field.
1651 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1652 L<Time::Local> and L<Date::Parse> for conversion functions.
1656 unsquelch_cdr - overrides any per customer cdr squelching when true
1662 my( $self, %params ) = @_;
1663 my $today = $params{today} ? $params{today} : time;
1664 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1667 my $format = $params{format};
1668 die "Unknown format: $format"
1669 unless $format =~ /^(latex|html|template)$/;
1671 my $cust_main = $self->cust_main;
1672 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1673 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1676 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1677 'html' => [ '<%=', '%>' ],
1678 'template' => [ '{', '}' ],
1681 #create the template
1682 my $template = $params{template} ? $params{template} : $self->_agent_template;
1683 my $templatefile = "invoice_$format";
1684 $templatefile .= "_$template"
1685 if length($template);
1686 my @invoice_template = map "$_\n", $conf->config($templatefile)
1687 or die "cannot load config data $templatefile";
1690 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1691 #change this to a die when the old code is removed
1692 warn "old-style invoice template $templatefile; ".
1693 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1694 $old_latex = 'true';
1695 @invoice_template = _translate_old_latex_format(@invoice_template);
1698 my $text_template = new Text::Template(
1700 SOURCE => \@invoice_template,
1701 DELIMITERS => $delimiters{$format},
1704 $text_template->compile()
1705 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1708 # additional substitution could possibly cause breakage in existing templates
1709 my %convert_maps = (
1711 'notes' => sub { map "$_", @_ },
1712 'footer' => sub { map "$_", @_ },
1713 'smallfooter' => sub { map "$_", @_ },
1714 'returnaddress' => sub { map "$_", @_ },
1715 'coupon' => sub { map "$_", @_ },
1721 s/%%(.*)$/<!-- $1 -->/g;
1722 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1723 s/\\begin\{enumerate\}/<ol>/g;
1725 s/\\end\{enumerate\}/<\/ol>/g;
1726 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1735 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1737 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1742 s/\\\\\*?\s*$/<BR>/;
1743 s/\\hyphenation\{[\w\s\-]+}//;
1747 'coupon' => sub { "" },
1754 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1755 s/\\begin\{enumerate\}//g;
1757 s/\\end\{enumerate\}//g;
1758 s/\\textbf\{(.*)\}/$1/g;
1765 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1767 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1772 s/\\\\\*?\s*$/\n/; # dubious
1773 s/\\hyphenation\{[\w\s\-]+}//;
1777 'coupon' => sub { "" },
1782 # hashes for differing output formats
1783 my %nbsps = ( 'latex' => '~',
1784 'html' => '', # '&nbps;' would be nice
1785 'template' => '', # not used
1787 my $nbsp = $nbsps{$format};
1789 my %escape_functions = ( 'latex' => \&_latex_escape,
1790 'html' => \&encode_entities,
1791 'template' => sub { shift },
1793 my $escape_function = $escape_functions{$format};
1795 my %date_formats = ( 'latex' => '%b %o, %Y',
1796 'html' => '%b %o, %Y',
1799 my $date_format = $date_formats{$format};
1801 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1803 'html' => sub { return '<b>'. shift(). '</b>'
1805 'template' => sub { shift },
1807 my $embolden_function = $embolden_functions{$format};
1810 # generate template variables
1813 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1817 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1823 $returnaddress = join("\n",
1824 $conf->config_orbase("invoice_${format}returnaddress", $template)
1827 } elsif ( grep /\S/,
1828 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1830 my $convert_map = $convert_maps{$format}{'returnaddress'};
1833 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1838 } elsif ( grep /\S/, $conf->config('company_address') ) {
1840 my $convert_map = $convert_maps{$format}{'returnaddress'};
1841 $returnaddress = join( "\n", &$convert_map(
1842 map { s/( {2,})/'~' x length($1)/eg;
1846 ( $conf->config('company_name'),
1847 $conf->config('company_address'),
1854 my $warning = "Couldn't find a return address; ".
1855 "do you need to set the company_address configuration value?";
1857 $returnaddress = $nbsp;
1858 #$returnaddress = $warning;
1862 my %invoice_data = (
1863 'company_name' => scalar( $conf->config('company_name') ),
1864 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1865 'custnum' => $self->custnum,
1866 'invnum' => $self->invnum,
1867 'date' => time2str($date_format, $self->_date),
1868 'today' => time2str('%b %o, %Y', $today),
1869 'agent' => &$escape_function($cust_main->agent->agent),
1870 'agent_custid' => &$escape_function($cust_main->agent_custid),
1871 'payname' => &$escape_function($cust_main->payname),
1872 'company' => &$escape_function($cust_main->company),
1873 'address1' => &$escape_function($cust_main->address1),
1874 'address2' => &$escape_function($cust_main->address2),
1875 'city' => &$escape_function($cust_main->city),
1876 'state' => &$escape_function($cust_main->state),
1877 'zip' => &$escape_function($cust_main->zip),
1878 'returnaddress' => $returnaddress,
1880 'terms' => $self->terms,
1881 'template' => $params{'template'},
1882 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1883 # better hang on to conf_dir for a while
1884 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1887 'current_charges' => sprintf("%.2f", $self->charged),
1888 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1889 'ship_enable' => $conf->exists('invoice-ship_address'),
1890 'unitprices' => $conf->exists('invoice-unitprice'),
1893 my $countrydefault = $conf->config('countrydefault') || 'US';
1894 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1895 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1896 my $method = $prefix.$_;
1897 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1899 $invoice_data{'ship_country'} = ''
1900 if ( $invoice_data{'ship_country'} eq $countrydefault );
1902 $invoice_data{'cid'} = $params{'cid'}
1905 if ( $cust_main->country eq $countrydefault ) {
1906 $invoice_data{'country'} = '';
1908 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1912 $invoice_data{'address'} = \@address;
1914 $cust_main->payname.
1915 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1916 ? " (P.O. #". $cust_main->payinfo. ")"
1920 push @address, $cust_main->company
1921 if $cust_main->company;
1922 push @address, $cust_main->address1;
1923 push @address, $cust_main->address2
1924 if $cust_main->address2;
1926 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1927 push @address, $invoice_data{'country'}
1928 if $invoice_data{'country'};
1930 while (scalar(@address) < 5);
1932 $invoice_data{'logo_file'} = $params{'logo_file'}
1933 if $params{'logo_file'};
1935 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1936 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1937 #my $balance_due = $self->owed + $pr_total - $cr_total;
1938 my $balance_due = $self->owed + $pr_total;
1939 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1940 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1942 #do variable substitution in notes, footer, smallfooter
1943 foreach my $include (qw( notes footer smallfooter coupon )) {
1945 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1948 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1950 @inc_src = $conf->config($inc_file);
1954 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1956 my $convert_map = $convert_maps{$format}{$include};
1958 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1959 s/--\@\]/$delimiters{$format}[1]/g;
1962 &$convert_map( $conf->config($inc_file) );
1966 my $inc_tt = new Text::Template (
1968 SOURCE => [ map "$_\n", @inc_src ],
1969 DELIMITERS => $delimiters{$format},
1970 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1972 unless ( $inc_tt->compile() ) {
1973 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1974 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1978 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1980 $invoice_data{$include} =~ s/\n+$//
1981 if ($format eq 'latex');
1984 $invoice_data{'po_line'} =
1985 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1986 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1989 my %money_chars = ( 'latex' => '',
1990 'html' => $conf->config('money_char') || '$',
1993 my $money_char = $money_chars{$format};
1995 my %other_money_chars = ( 'latex' => '\dollar ',
1996 'html' => $conf->config('money_char') || '$',
1999 my $other_money_char = $other_money_chars{$format};
2001 my @detail_items = ();
2002 my @total_items = ();
2006 $invoice_data{'detail_items'} = \@detail_items;
2007 $invoice_data{'total_items'} = \@total_items;
2008 $invoice_data{'buf'} = \@buf;
2009 $invoice_data{'sections'} = \@sections;
2011 my $previous_section = { 'description' => 'Previous Charges',
2012 'subtotal' => $other_money_char.
2013 sprintf('%.2f', $pr_total),
2017 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2018 'subtotal' => $taxtotal }; # adjusted below
2020 my $adjusttotal = 0;
2021 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2022 'subtotal' => 0 }; # adjusted below
2024 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2025 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2026 my $late_sections = [];
2027 if ( $multisection ) {
2028 push @sections, $self->_items_sections( $late_sections );
2030 push @sections, { 'description' => '', 'subtotal' => '' };
2033 foreach my $line_item ( $conf->exists('disable_previous_balance')
2035 : $self->_items_previous
2039 ext_description => [],
2041 $detail->{'ref'} = $line_item->{'pkgnum'};
2042 $detail->{'quantity'} = 1;
2043 $detail->{'section'} = $previous_section;
2044 $detail->{'description'} = &$escape_function($line_item->{'description'});
2045 if ( exists $line_item->{'ext_description'} ) {
2046 @{$detail->{'ext_description'}} = map {
2047 &$escape_function($_);
2048 } @{$line_item->{'ext_description'}};
2050 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2051 $line_item->{'amount'};
2052 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2054 push @detail_items, $detail;
2055 push @buf, [ $detail->{'description'},
2056 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2060 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2061 push @buf, ['','-----------'];
2062 push @buf, [ 'Total Previous Balance',
2063 $money_char. sprintf("%10.2f", $pr_total) ];
2067 foreach my $section (@sections, @$late_sections) {
2069 $section->{'subtotal'} = $other_money_char.
2070 sprintf('%.2f', $section->{'subtotal'})
2073 if ( $section->{'description'} ) {
2074 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2080 $options{'section'} = $section if $multisection;
2081 $options{'format'} = $format;
2082 $options{'escape_function'} = $escape_function;
2083 $options{'format_function'} = sub { () } unless $unsquelched;
2084 $options{'unsquelched'} = $unsquelched;
2086 foreach my $line_item ( $self->_items_pkg(%options) ) {
2088 ext_description => [],
2090 $detail->{'ref'} = $line_item->{'pkgnum'};
2091 $detail->{'quantity'} = $line_item->{'quantity'};
2092 $detail->{'section'} = $section;
2093 $detail->{'description'} = &$escape_function($line_item->{'description'});
2094 if ( exists $line_item->{'ext_description'} ) {
2095 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2097 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2098 $line_item->{'amount'};
2099 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2100 $line_item->{'unit_amount'};
2101 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2103 push @detail_items, $detail;
2104 push @buf, ( [ $detail->{'description'},
2105 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2107 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2111 if ( $section->{'description'} ) {
2112 push @buf, ( ['','-----------'],
2113 [ $section->{'description'}. ' sub-total',
2114 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2123 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2124 unshift @sections, $previous_section if $pr_total;
2127 foreach my $tax ( $self->_items_tax ) {
2129 $total->{'total_item'} = &$escape_function($tax->{'description'});
2130 $taxtotal += $tax->{'amount'};
2131 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2132 if ( $multisection ) {
2133 my $money = $old_latex ? '' : $money_char;
2134 push @detail_items, {
2135 ext_description => [],
2138 description => &$escape_function($tax->{'description'}),
2139 amount => $money. $tax->{'amount'},
2141 section => $tax_section,
2144 push @total_items, $total;
2146 push @buf,[ $total->{'total_item'},
2147 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2154 $total->{'total_item'} = 'Sub-total';
2155 $total->{'total_amount'} =
2156 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2158 if ( $multisection ) {
2159 $tax_section->{'subtotal'} = $other_money_char.
2160 sprintf('%.2f', $taxtotal);
2161 $tax_section->{'pretotal'} = 'New charges sub-total '.
2162 $total->{'total_amount'};
2163 push @sections, $tax_section if $taxtotal;
2165 unshift @total_items, $total;
2168 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2170 push @buf,['','-----------'];
2171 push @buf,[( $conf->exists('disable_previous_balance')
2173 : 'Total New Charges'
2175 $money_char. sprintf("%10.2f",$self->charged) ];
2180 $total->{'total_item'} = &$embolden_function('Total');
2181 $total->{'total_amount'} =
2182 &$embolden_function(
2185 $self->charged + ( $conf->exists('disable_previous_balance')
2191 if ( $multisection ) {
2192 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2193 sprintf('%.2f', $self->charged );
2195 push @total_items, $total;
2197 push @buf,['','-----------'];
2198 push @buf,['Total Charges',
2200 sprintf( '%10.2f', $self->charged +
2201 ( $conf->exists('disable_previous_balance')
2210 unless ( $conf->exists('disable_previous_balance') ) {
2211 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2214 my $credittotal = 0;
2215 foreach my $credit ( $self->_items_credits ) {
2217 $total->{'total_item'} = &$escape_function($credit->{'description'});
2218 $credittotal += $credit->{'amount'};
2219 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2220 $adjusttotal += $credit->{'amount'};
2221 if ( $multisection ) {
2222 my $money = $old_latex ? '' : $money_char;
2223 push @detail_items, {
2224 ext_description => [],
2227 description => &$escape_function($credit->{'description'}),
2228 amount => $money. $credit->{'amount'},
2230 section => $adjust_section,
2233 push @total_items, $total;
2236 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2239 foreach ( $self->cust_credited ) {
2241 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2243 my $reason = substr($_->cust_credit->reason,0,32);
2244 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2245 $reason = " ($reason) " if $reason;
2247 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2248 $money_char. sprintf("%10.2f",$_->amount)
2253 my $paymenttotal = 0;
2254 foreach my $payment ( $self->_items_payments ) {
2256 $total->{'total_item'} = &$escape_function($payment->{'description'});
2257 $paymenttotal += $payment->{'amount'};
2258 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2259 $adjusttotal += $payment->{'amount'};
2260 if ( $multisection ) {
2261 my $money = $old_latex ? '' : $money_char;
2262 push @detail_items, {
2263 ext_description => [],
2266 description => &$escape_function($payment->{'description'}),
2267 amount => $money. $payment->{'amount'},
2269 section => $adjust_section,
2272 push @total_items, $total;
2274 push @buf, [ $payment->{'description'},
2275 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2278 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2280 if ( $multisection ) {
2281 $adjust_section->{'subtotal'} = $other_money_char.
2282 sprintf('%.2f', $adjusttotal);
2283 push @sections, $adjust_section;
2288 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2289 $total->{'total_amount'} =
2290 &$embolden_function(
2291 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2293 if ( $multisection ) {
2294 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2295 $total->{'total_amount'};
2297 push @total_items, $total;
2299 push @buf,['','-----------'];
2300 push @buf,[$self->balance_due_msg, $money_char.
2301 sprintf("%10.2f", $balance_due ) ];
2305 if ( $multisection ) {
2306 push @sections, @$late_sections
2312 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2313 /invoice_lines\((\d*)\)/;
2314 $invoice_lines += $1 || scalar(@buf);
2317 die "no invoice_lines() functions in template?"
2318 if ( $format eq 'template' && !$wasfunc );
2320 if ($format eq 'template') {
2322 if ( $invoice_lines ) {
2323 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2324 $invoice_data{'total_pages'}++
2325 if scalar(@buf) % $invoice_lines;
2328 #setup subroutine for the template
2329 sub FS::cust_bill::_template::invoice_lines {
2330 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2332 scalar(@FS::cust_bill::_template::buf)
2333 ? shift @FS::cust_bill::_template::buf
2342 push @collect, split("\n",
2343 $text_template->fill_in( HASH => \%invoice_data,
2344 PACKAGE => 'FS::cust_bill::_template'
2347 $FS::cust_bill::_template::page++;
2349 map "$_\n", @collect;
2351 warn "filling in template for invoice ". $self->invnum. "\n"
2353 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2356 $text_template->fill_in(HASH => \%invoice_data);
2360 =item print_ps [ TIME [ , TEMPLATE ] ]
2362 Returns an postscript invoice, as a scalar.
2364 TIME an optional value used to control the printing of overdue messages. The
2365 default is now. It isn't the date of the invoice; that's the `_date' field.
2366 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2367 L<Time::Local> and L<Date::Parse> for conversion functions.
2374 my ($file, $lfile) = $self->print_latex(@_);
2375 my $ps = generate_ps($file);
2381 =item print_pdf [ TIME [ , TEMPLATE ] ]
2383 Returns an PDF invoice, as a scalar.
2385 TIME an optional value used to control the printing of overdue messages. The
2386 default is now. It isn't the date of the invoice; that's the `_date' field.
2387 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2388 L<Time::Local> and L<Date::Parse> for conversion functions.
2395 my ($file, $lfile) = $self->print_latex(@_);
2396 my $pdf = generate_pdf($file);
2402 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2404 Returns an HTML invoice, as a scalar.
2406 TIME an optional value used to control the printing of overdue messages. The
2407 default is now. It isn't the date of the invoice; that's the `_date' field.
2408 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2409 L<Time::Local> and L<Date::Parse> for conversion functions.
2411 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2412 when emailing the invoice as part of a multipart/related MIME email.
2420 %params = %{ shift() };
2422 $params{'time'} = shift;
2423 $params{'template'} = shift;
2424 $params{'cid'} = shift;
2427 $params{'format'} = 'html';
2429 $self->print_generic( %params );
2432 # quick subroutine for print_latex
2434 # There are ten characters that LaTeX treats as special characters, which
2435 # means that they do not simply typeset themselves:
2436 # # $ % & ~ _ ^ \ { }
2438 # TeX ignores blanks following an escaped character; if you want a blank (as
2439 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2443 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2444 $value =~ s/([<>])/\$$1\$/g;
2448 #utility methods for print_*
2450 sub _translate_old_latex_format {
2451 warn "_translate_old_latex_format called\n"
2458 if ( $line =~ /^%%Detail\s*$/ ) {
2460 push @template, q![@--!,
2461 q! foreach my $_tr_line (@detail_items) {!,
2462 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2463 q! $_tr_line->{'description'} .= !,
2464 q! "\\tabularnewline\n~~".!,
2465 q! join( "\\tabularnewline\n~~",!,
2466 q! @{$_tr_line->{'ext_description'}}!,
2470 while ( ( my $line_item_line = shift )
2471 !~ /^%%EndDetail\s*$/ ) {
2472 $line_item_line =~ s/'/\\'/g; # nice LTS
2473 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2474 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2475 push @template, " \$OUT .= '$line_item_line';";
2478 push @template, '}',
2481 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2483 push @template, '[@--',
2484 ' foreach my $_tr_line (@total_items) {';
2486 while ( ( my $total_item_line = shift )
2487 !~ /^%%EndTotalDetails\s*$/ ) {
2488 $total_item_line =~ s/'/\\'/g; # nice LTS
2489 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2490 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2491 push @template, " \$OUT .= '$total_item_line';";
2494 push @template, '}',
2498 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2499 push @template, $line;
2505 warn "$_\n" foreach @template;
2514 #check for an invoice- specific override (eventually)
2516 #check for a customer- specific override
2517 return $self->cust_main->invoice_terms
2518 if $self->cust_main->invoice_terms;
2520 #use configured default or default default
2521 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2527 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2528 $duedate = $self->_date() + ( $1 * 86400 );
2535 $self->due_date ? time2str(shift, $self->due_date) : '';
2538 sub balance_due_msg {
2540 my $msg = 'Balance Due';
2541 return $msg unless $self->terms;
2542 if ( $self->due_date ) {
2543 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2544 } elsif ( $self->terms ) {
2545 $msg .= ' - '. $self->terms;
2550 sub balance_due_date {
2553 if ( $conf->exists('invoice_default_terms')
2554 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2555 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2560 =item invnum_date_pretty
2562 Returns a string with the invoice number and date, for example:
2563 "Invoice #54 (3/20/2008)"
2567 sub invnum_date_pretty {
2569 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2572 sub _items_sections {
2579 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2582 if ( $cust_bill_pkg->pkgnum > 0 ) {
2584 my $desc = $cust_bill_pkg->section;
2585 my $dup_desc = $cust_bill_pkg->duplicate_section;
2587 if ($cust_bill_pkg->duplicate) {
2588 $s{$dup_desc} += $cust_bill_pkg->setup
2589 if ( $cust_bill_pkg->setup != 0 );
2591 $s{$dup_desc} += $cust_bill_pkg->recur
2592 if ( $cust_bill_pkg->recur != 0 );
2595 if ( $cust_bill_pkg->post_total ) {
2596 $l{$desc} += $cust_bill_pkg->setup
2597 if ( $cust_bill_pkg->setup != 0 );
2599 $l{$desc} += $cust_bill_pkg->recur
2600 if ( $cust_bill_pkg->recur != 0 );
2603 $s{$desc} += $cust_bill_pkg->setup
2604 if ( $cust_bill_pkg->setup != 0 );
2606 $s{$desc} += $cust_bill_pkg->recur
2607 if ( $cust_bill_pkg->recur != 0 );
2614 push @$late, map { { 'description' => $_,
2615 'subtotal' => $l{$_},
2619 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2626 #my @display = scalar(@_)
2628 # : qw( _items_previous _items_pkg );
2629 # #: qw( _items_pkg );
2630 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2631 my @display = qw( _items_previous _items_pkg );
2634 foreach my $display ( @display ) {
2635 push @b, $self->$display(@_);
2640 sub _items_previous {
2642 my $cust_main = $self->cust_main;
2643 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2645 foreach ( @pr_cust_bill ) {
2647 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2648 ' ('. time2str('%x',$_->_date). ')',
2649 #'pkgpart' => 'N/A',
2651 'amount' => sprintf("%.2f", $_->owed),
2657 # 'description' => 'Previous Balance',
2658 # #'pkgpart' => 'N/A',
2659 # 'pkgnum' => 'N/A',
2660 # 'amount' => sprintf("%10.2f", $pr_total ),
2661 # 'ext_description' => [ map {
2662 # "Invoice ". $_->invnum.
2663 # " (". time2str("%x",$_->_date). ") ".
2664 # sprintf("%10.2f", $_->owed)
2665 # } @pr_cust_bill ],
2673 my $section = $options{'section'};
2674 my $desc = $section->{'description'};
2676 grep { $_->pkgnum &&
2678 ? ( $_->section eq $desc || $_->duplicate_section eq $desc )
2681 } $self->cust_bill_pkg;
2682 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2686 return 0 unless $a cmp $b;
2687 return -1 if $b eq 'Tax';
2688 return 1 if $a eq 'Tax';
2689 return -1 if $b eq 'Other surcharges';
2690 return 1 if $a eq 'Other surcharges';
2696 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2697 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2700 sub _items_cust_bill_pkg {
2702 my $cust_bill_pkg = shift;
2705 my $format = $opt{format} || '';
2706 my $escape_function = $opt{escape_function} || sub { shift };
2707 my $format_function = $opt{format_function} || '';
2708 my $unsquelched = $opt{unsquelched} || '';
2711 my $last_pkgnum = '';
2712 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2715 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2717 my $desc = $cust_bill_pkg->desc;
2719 my %details_opt = ( 'format' => $format,
2720 'escape_function' => $escape_function,
2721 'format_function' => $format_function,
2724 if ( $cust_bill_pkg->pkgnum > 0 ) {
2726 if ( $cust_bill_pkg->setup != 0 ) {
2728 my $description = $desc;
2729 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2731 my @d = map &{$escape_function}($_),
2732 $cust_pkg->h_labels_short($self->_date);
2733 push @d, $cust_bill_pkg->details(%details_opt)
2734 if $cust_bill_pkg->recur == 0;
2737 description => $description,
2738 #pkgpart => $part_pkg->pkgpart,
2739 pkgnum => $cust_bill_pkg->pkgnum,
2740 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2741 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2742 quantity => $cust_bill_pkg->quantity,
2743 ext_description => \@d,
2750 if ( $cust_bill_pkg->recur != 0 ) {
2753 ( $cust_bill_pkg->duplicate &&
2754 $opt{section}->{description} ne $cust_bill_pkg->section
2756 my $description = $is_summary ? "Usage charges" : $desc;
2758 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2759 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2760 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2763 #at least until cust_bill_pkg has "past" ranges in addition to
2764 #the "future" sdate/edate ones... see #3032
2766 push @d, map &{$escape_function}($_),
2767 $cust_pkg->h_labels_short($self->_date)
2768 #$cust_bill_pkg->edate,
2769 #$cust_bill_pkg->sdate),
2770 unless ($cust_bill_pkg->pkgnum eq $last_pkgnum);
2772 @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2773 push @d, $cust_bill_pkg->details(%details_opt)
2776 if ($cust_bill_pkg->pkgnum eq $last_pkgnum) {
2779 sprintf("%.2f", $b[$#b]->{amount} + $cust_bill_pkg->recur);
2780 push @{$b[$#b]->{ext_description}}, @d;
2785 description => $description,
2786 #pkgpart => $part_pkg->pkgpart,
2787 pkgnum => $cust_bill_pkg->pkgnum,
2788 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2789 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2790 quantity => $cust_bill_pkg->quantity,
2791 ext_description => \@d,
2796 if ($conf->exists('separate_usage') && $cust_bill_pkg->type ne 'U') {
2799 $last_pkgnum = $cust_bill_pkg->pkgnum;
2803 } else { #pkgnum tax or one-shot line item (??)
2805 if ( $cust_bill_pkg->setup != 0 ) {
2807 'description' => $desc,
2808 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2811 if ( $cust_bill_pkg->recur != 0 ) {
2813 'description' => "$desc (".
2814 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2815 time2str("%x", $cust_bill_pkg->edate). ')',
2816 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2830 sub _items_credits {
2835 foreach ( $self->cust_credited ) {
2837 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2839 my $reason = $_->cust_credit->reason;
2840 #my $reason = substr($_->cust_credit->reason,0,32);
2841 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2842 $reason = " ($reason) " if $reason;
2844 #'description' => 'Credit ref\#'. $_->crednum.
2845 # " (". time2str("%x",$_->cust_credit->_date) .")".
2847 'description' => 'Credit applied '.
2848 time2str("%x",$_->cust_credit->_date). $reason,
2849 'amount' => sprintf("%.2f",$_->amount),
2852 #foreach ( @cr_cust_credit ) {
2854 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2855 # $money_char. sprintf("%10.2f",$_->credited)
2863 sub _items_payments {
2867 #get & print payments
2868 foreach ( $self->cust_bill_pay ) {
2870 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2873 'description' => "Payment received ".
2874 time2str("%x",$_->cust_pay->_date ),
2875 'amount' => sprintf("%.2f", $_->amount )
2894 sub process_reprint {
2895 process_re_X('print', @_);
2902 sub process_reemail {
2903 process_re_X('email', @_);
2911 process_re_X('fax', @_);
2919 process_re_X('ftp', @_);
2922 use Storable qw(thaw);
2926 my( $method, $job ) = ( shift, shift );
2927 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2929 my $param = thaw(decode_base64(shift));
2930 warn Dumper($param) if $DEBUG;
2941 my($method, $job, %param ) = @_;
2943 warn "re_X $method for job $job with param:\n".
2944 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2947 #some false laziness w/search/cust_bill.html
2949 my $orderby = 'ORDER BY cust_bill._date';
2951 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2953 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2955 my @cust_bill = qsearch( {
2956 #'select' => "cust_bill.*",
2957 'table' => 'cust_bill',
2958 'addl_from' => $addl_from,
2960 'extra_sql' => $extra_sql,
2961 'order_by' => $orderby,
2965 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2967 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2970 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2971 foreach my $cust_bill ( @cust_bill ) {
2972 $cust_bill->$method();
2974 if ( $job ) { #progressbar foo
2976 if ( time - $min_sec > $last ) {
2977 my $error = $job->update_statustext(
2978 int( 100 * $num / scalar(@cust_bill) )
2980 die $error if $error;
2991 =head1 CLASS METHODS
2997 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3003 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3008 Returns an SQL fragment to retreive the net amount (charged minus credited).
3014 'charged - '. $class->credited_sql;
3019 Returns an SQL fragment to retreive the amount paid against this invoice.
3025 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3026 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3031 Returns an SQL fragment to retreive the amount credited against this invoice.
3037 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3038 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3041 =item search_sql HASHREF
3043 Class method which returns an SQL WHERE fragment to search for parameters
3044 specified in HASHREF. Valid parameters are
3050 Epoch date (UNIX timestamp) setting a lower bound for _date values
3054 Epoch date (UNIX timestamp) setting an upper bound for _date values
3068 =item newest_percust
3072 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3077 my($class, $param) = @_;
3079 warn "$me search_sql called with params: \n".
3080 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3085 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3086 push @search, "cust_bill._date >= $1";
3088 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3089 push @search, "cust_bill._date < $1";
3091 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3092 push @search, "cust_bill.invnum >= $1";
3094 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3095 push @search, "cust_bill.invnum <= $1";
3097 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3098 push @search, "cust_main.agentnum = $1";
3101 push @search, '0 != '. FS::cust_bill->owed_sql
3102 if $param->{'open'};
3104 push @search, '0 != '. FS::cust_bill->net_sql
3107 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3108 if $param->{'days'};
3110 if ( $param->{'newest_percust'} ) {
3112 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3113 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3115 my @newest_where = map { my $x = $_;
3116 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3119 grep ! /^cust_main./, @search;
3120 my $newest_where = scalar(@newest_where)
3121 ? ' AND '. join(' AND ', @newest_where)
3125 push @search, "cust_bill._date = (
3126 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3127 WHERE newest_cust_bill.custnum = cust_bill.custnum
3133 my $curuser = $FS::CurrentUser::CurrentUser;
3134 if ( $curuser->username eq 'fs_queue'
3135 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3137 my $newuser = qsearchs('access_user', {
3138 'username' => $username,
3142 $curuser = $newuser;
3144 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3148 push @search, $curuser->agentnums_sql;
3150 join(' AND ', @search );
3162 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3163 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base