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;
2558 my $dup_desc = $cust_bill_pkg->duplicate_section;
2560 if ($cust_bill_pkg->duplicate) {
2561 $s{$dup_desc} += $cust_bill_pkg->setup
2562 if ( $cust_bill_pkg->setup != 0 );
2564 $s{$dup_desc} += $cust_bill_pkg->recur
2565 if ( $cust_bill_pkg->recur != 0 );
2568 if ( $cust_bill_pkg->post_total ) {
2569 $l{$desc} += $cust_bill_pkg->setup
2570 if ( $cust_bill_pkg->setup != 0 );
2572 $l{$desc} += $cust_bill_pkg->recur
2573 if ( $cust_bill_pkg->recur != 0 );
2576 $s{$desc} += $cust_bill_pkg->setup
2577 if ( $cust_bill_pkg->setup != 0 );
2579 $s{$desc} += $cust_bill_pkg->recur
2580 if ( $cust_bill_pkg->recur != 0 );
2587 push @$late, map { { 'description' => $_,
2588 'subtotal' => $l{$_},
2592 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2599 #my @display = scalar(@_)
2601 # : qw( _items_previous _items_pkg );
2602 # #: qw( _items_pkg );
2603 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2604 my @display = qw( _items_previous _items_pkg );
2607 foreach my $display ( @display ) {
2608 push @b, $self->$display(@_);
2613 sub _items_previous {
2615 my $cust_main = $self->cust_main;
2616 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2618 foreach ( @pr_cust_bill ) {
2620 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2621 ' ('. time2str('%x',$_->_date). ')',
2622 #'pkgpart' => 'N/A',
2624 'amount' => sprintf("%.2f", $_->owed),
2630 # 'description' => 'Previous Balance',
2631 # #'pkgpart' => 'N/A',
2632 # 'pkgnum' => 'N/A',
2633 # 'amount' => sprintf("%10.2f", $pr_total ),
2634 # 'ext_description' => [ map {
2635 # "Invoice ". $_->invnum.
2636 # " (". time2str("%x",$_->_date). ") ".
2637 # sprintf("%10.2f", $_->owed)
2638 # } @pr_cust_bill ],
2646 my $section = $options{'section'};
2647 my $desc = $section->{'description'};
2649 grep { $_->pkgnum &&
2651 ? ( $_->section eq $desc || $_->duplicate_section eq $desc )
2654 } $self->cust_bill_pkg;
2655 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2659 return 0 unless $a cmp $b;
2660 return -1 if $b eq 'Tax';
2661 return 1 if $a eq 'Tax';
2662 return -1 if $b eq 'Other surcharges';
2663 return 1 if $a eq 'Other surcharges';
2669 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2670 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2673 sub _items_cust_bill_pkg {
2675 my $cust_bill_pkg = shift;
2678 my $format = $opt{format} || '';
2679 my $escape_function = $opt{escape_function} || sub { shift };
2680 my $format_function = $opt{format_function} || '';
2681 my $unsquelched = $opt{unsquelched} || '';
2684 my $last_pkgnum = '';
2685 foreach my $cust_bill_pkg ( grep { $unsquelched ? 1 : ! $_->separate_cdr }
2690 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2692 my $desc = $cust_bill_pkg->desc;
2694 my %details_opt = ( 'format' => $format,
2695 'escape_function' => $escape_function,
2696 'format_function' => $format_function,
2699 if ( $cust_bill_pkg->pkgnum > 0 ) {
2701 if ( $cust_bill_pkg->setup != 0 ) {
2703 my $description = $desc;
2704 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2706 my @d = map &{$escape_function}($_),
2707 $cust_pkg->h_labels_short($self->_date);
2708 push @d, $cust_bill_pkg->details(%details_opt)
2709 if $cust_bill_pkg->recur == 0;
2712 description => $description,
2713 #pkgpart => $part_pkg->pkgpart,
2714 pkgnum => $cust_bill_pkg->pkgnum,
2715 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2716 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2717 quantity => $cust_bill_pkg->quantity,
2718 ext_description => \@d,
2725 if ( $cust_bill_pkg->recur != 0 ) {
2728 ( $cust_bill_pkg->duplicate &&
2729 $opt{section}->{description} ne $cust_bill_pkg->section
2731 my $description = $is_summary ? "Usage charges" : $desc;
2733 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2734 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2735 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2738 #at least until cust_bill_pkg has "past" ranges in addition to
2739 #the "future" sdate/edate ones... see #3032
2741 push @d, map &{$escape_function}($_),
2742 $cust_pkg->h_labels_short($self->_date)
2743 #$cust_bill_pkg->edate,
2744 #$cust_bill_pkg->sdate),
2745 unless ($cust_bill_pkg->pkgnum eq $last_pkgnum);
2747 @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2748 push @d, $cust_bill_pkg->details(%details_opt)
2751 if ($cust_bill_pkg->pkgnum eq $last_pkgnum) {
2754 sprintf("%.2f", $b[$#b]->{amount} + $cust_bill_pkg->recur);
2755 push @{$b[$#b]->{ext_description}}, @d;
2760 description => $description,
2761 #pkgpart => $part_pkg->pkgpart,
2762 pkgnum => $cust_bill_pkg->pkgnum,
2763 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2764 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2765 quantity => $cust_bill_pkg->quantity,
2766 ext_description => \@d,
2771 if ($conf->exists('separate_usage') && $cust_bill_pkg->type ne 'U') {
2774 $last_pkgnum = $cust_bill_pkg->pkgnum;
2778 } else { #pkgnum tax or one-shot line item (??)
2780 if ( $cust_bill_pkg->setup != 0 ) {
2782 'description' => $desc,
2783 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2786 if ( $cust_bill_pkg->recur != 0 ) {
2788 'description' => "$desc (".
2789 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2790 time2str("%x", $cust_bill_pkg->edate). ')',
2791 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2805 sub _items_credits {
2810 foreach ( $self->cust_credited ) {
2812 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2814 my $reason = $_->cust_credit->reason;
2815 #my $reason = substr($_->cust_credit->reason,0,32);
2816 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2817 $reason = " ($reason) " if $reason;
2819 #'description' => 'Credit ref\#'. $_->crednum.
2820 # " (". time2str("%x",$_->cust_credit->_date) .")".
2822 'description' => 'Credit applied '.
2823 time2str("%x",$_->cust_credit->_date). $reason,
2824 'amount' => sprintf("%.2f",$_->amount),
2827 #foreach ( @cr_cust_credit ) {
2829 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2830 # $money_char. sprintf("%10.2f",$_->credited)
2838 sub _items_payments {
2842 #get & print payments
2843 foreach ( $self->cust_bill_pay ) {
2845 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2848 'description' => "Payment received ".
2849 time2str("%x",$_->cust_pay->_date ),
2850 'amount' => sprintf("%.2f", $_->amount )
2869 sub process_reprint {
2870 process_re_X('print', @_);
2877 sub process_reemail {
2878 process_re_X('email', @_);
2886 process_re_X('fax', @_);
2889 use Storable qw(thaw);
2893 my( $method, $job ) = ( shift, shift );
2894 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2896 my $param = thaw(decode_base64(shift));
2897 warn Dumper($param) if $DEBUG;
2908 my($method, $job, %param ) = @_;
2910 warn "re_X $method for job $job with param:\n".
2911 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2914 #some false laziness w/search/cust_bill.html
2916 my $orderby = 'ORDER BY cust_bill._date';
2918 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2920 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2922 my @cust_bill = qsearch( {
2923 #'select' => "cust_bill.*",
2924 'table' => 'cust_bill',
2925 'addl_from' => $addl_from,
2927 'extra_sql' => $extra_sql,
2928 'order_by' => $orderby,
2932 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2935 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2936 foreach my $cust_bill ( @cust_bill ) {
2937 $cust_bill->$method();
2939 if ( $job ) { #progressbar foo
2941 if ( time - $min_sec > $last ) {
2942 my $error = $job->update_statustext(
2943 int( 100 * $num / scalar(@cust_bill) )
2945 die $error if $error;
2956 =head1 CLASS METHODS
2962 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2968 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2973 Returns an SQL fragment to retreive the net amount (charged minus credited).
2979 'charged - '. $class->credited_sql;
2984 Returns an SQL fragment to retreive the amount paid against this invoice.
2990 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2991 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2996 Returns an SQL fragment to retreive the amount credited against this invoice.
3002 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3003 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3006 =item search_sql HASHREF
3008 Class method which returns an SQL WHERE fragment to search for parameters
3009 specified in HASHREF. Valid parameters are
3015 Epoch date (UNIX timestamp) setting a lower bound for _date values
3019 Epoch date (UNIX timestamp) setting an upper bound for _date values
3033 =item newest_percust
3037 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3042 my($class, $param) = @_;
3044 warn "$me search_sql called with params: \n".
3045 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3050 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3051 push @search, "cust_bill._date >= $1";
3053 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3054 push @search, "cust_bill._date < $1";
3056 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3057 push @search, "cust_bill.invnum >= $1";
3059 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3060 push @search, "cust_bill.invnum <= $1";
3062 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3063 push @search, "cust_main.agentnum = $1";
3066 push @search, '0 != '. FS::cust_bill->owed_sql
3067 if $param->{'open'};
3069 push @search, '0 != '. FS::cust_bill->net_sql
3072 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3073 if $param->{'days'};
3075 if ( $param->{'newest_percust'} ) {
3077 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3078 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3080 my @newest_where = map { my $x = $_;
3081 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3084 grep ! /^cust_main./, @search;
3085 my $newest_where = scalar(@newest_where)
3086 ? ' AND '. join(' AND ', @newest_where)
3090 push @search, "cust_bill._date = (
3091 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3092 WHERE newest_cust_bill.custnum = cust_bill.custnum
3098 my $curuser = $FS::CurrentUser::CurrentUser;
3099 if ( $curuser->username eq 'fs_queue'
3100 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3102 my $newuser = qsearchs('access_user', {
3103 'username' => $username,
3107 $curuser = $newuser;
3109 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3113 push @search, $curuser->agentnums_sql;
3115 join(' AND ', @search );
3127 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3128 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base