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.
1638 my( $self, %params ) = @_;
1639 my $today = $params{today} ? $params{today} : time;
1640 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1643 my $format = $params{format};
1644 die "Unknown format: $format"
1645 unless $format =~ /^(latex|html|template)$/;
1647 my $cust_main = $self->cust_main;
1648 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1649 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1652 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1653 'html' => [ '<%=', '%>' ],
1654 'template' => [ '{', '}' ],
1657 #create the template
1658 my $template = $params{template} ? $params{template} : $self->_agent_template;
1659 my $templatefile = "invoice_$format";
1660 $templatefile .= "_$template"
1661 if length($template);
1662 my @invoice_template = map "$_\n", $conf->config($templatefile)
1663 or die "cannot load config data $templatefile";
1666 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1667 #change this to a die when the old code is removed
1668 warn "old-style invoice template $templatefile; ".
1669 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1670 $old_latex = 'true';
1671 @invoice_template = _translate_old_latex_format(@invoice_template);
1674 my $text_template = new Text::Template(
1676 SOURCE => \@invoice_template,
1677 DELIMITERS => $delimiters{$format},
1680 $text_template->compile()
1681 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1684 # additional substitution could possibly cause breakage in existing templates
1685 my %convert_maps = (
1687 'notes' => sub { map "$_", @_ },
1688 'footer' => sub { map "$_", @_ },
1689 'smallfooter' => sub { map "$_", @_ },
1690 'returnaddress' => sub { map "$_", @_ },
1691 'coupon' => sub { map "$_", @_ },
1697 s/%%(.*)$/<!-- $1 -->/g;
1698 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1699 s/\\begin\{enumerate\}/<ol>/g;
1701 s/\\end\{enumerate\}/<\/ol>/g;
1702 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1711 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1713 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1718 s/\\\\\*?\s*$/<BR>/;
1719 s/\\hyphenation\{[\w\s\-]+}//;
1723 'coupon' => sub { "" },
1730 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1731 s/\\begin\{enumerate\}//g;
1733 s/\\end\{enumerate\}//g;
1734 s/\\textbf\{(.*)\}/$1/g;
1741 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1743 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1748 s/\\\\\*?\s*$/\n/; # dubious
1749 s/\\hyphenation\{[\w\s\-]+}//;
1753 'coupon' => sub { "" },
1758 # hashes for differing output formats
1759 my %nbsps = ( 'latex' => '~',
1760 'html' => '', # '&nbps;' would be nice
1761 'template' => '', # not used
1763 my $nbsp = $nbsps{$format};
1765 my %escape_functions = ( 'latex' => \&_latex_escape,
1766 'html' => \&encode_entities,
1767 'template' => sub { shift },
1769 my $escape_function = $escape_functions{$format};
1771 my %date_formats = ( 'latex' => '%b %o, %Y',
1772 'html' => '%b %o, %Y',
1775 my $date_format = $date_formats{$format};
1777 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1779 'html' => sub { return '<b>'. shift(). '</b>'
1781 'template' => sub { shift },
1783 my $embolden_function = $embolden_functions{$format};
1786 # generate template variables
1789 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1793 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1799 $returnaddress = join("\n",
1800 $conf->config_orbase("invoice_${format}returnaddress", $template)
1803 } elsif ( grep /\S/,
1804 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1806 my $convert_map = $convert_maps{$format}{'returnaddress'};
1809 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1814 } elsif ( grep /\S/, $conf->config('company_address') ) {
1816 my $convert_map = $convert_maps{$format}{'returnaddress'};
1817 $returnaddress = join( "\n", &$convert_map(
1818 map { s/( {2,})/'~' x length($1)/eg;
1822 ( $conf->config('company_name'),
1823 $conf->config('company_address'),
1830 my $warning = "Couldn't find a return address; ".
1831 "do you need to set the company_address configuration value?";
1833 $returnaddress = $nbsp;
1834 #$returnaddress = $warning;
1838 my %invoice_data = (
1839 'company_name' => scalar( $conf->config('company_name') ),
1840 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1841 'custnum' => $self->custnum,
1842 'invnum' => $self->invnum,
1843 'date' => time2str($date_format, $self->_date),
1844 'today' => time2str('%b %o, %Y', $today),
1845 'agent' => &$escape_function($cust_main->agent->agent),
1846 'agent_custid' => &$escape_function($cust_main->agent_custid),
1847 'payname' => &$escape_function($cust_main->payname),
1848 'company' => &$escape_function($cust_main->company),
1849 'address1' => &$escape_function($cust_main->address1),
1850 'address2' => &$escape_function($cust_main->address2),
1851 'city' => &$escape_function($cust_main->city),
1852 'state' => &$escape_function($cust_main->state),
1853 'zip' => &$escape_function($cust_main->zip),
1854 'returnaddress' => $returnaddress,
1856 'terms' => $self->terms,
1857 'template' => $params{'template'},
1858 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1859 # better hang on to conf_dir for a while
1860 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1863 'current_charges' => sprintf("%.2f", $self->charged),
1864 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1865 'ship_enable' => $conf->exists('invoice-ship_address'),
1866 'unitprices' => $conf->exists('invoice-unitprice'),
1869 my $countrydefault = $conf->config('countrydefault') || 'US';
1870 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1871 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1872 my $method = $prefix.$_;
1873 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1875 $invoice_data{'ship_country'} = ''
1876 if ( $invoice_data{'ship_country'} eq $countrydefault );
1878 $invoice_data{'cid'} = $params{'cid'}
1881 if ( $cust_main->country eq $countrydefault ) {
1882 $invoice_data{'country'} = '';
1884 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1888 $invoice_data{'address'} = \@address;
1890 $cust_main->payname.
1891 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1892 ? " (P.O. #". $cust_main->payinfo. ")"
1896 push @address, $cust_main->company
1897 if $cust_main->company;
1898 push @address, $cust_main->address1;
1899 push @address, $cust_main->address2
1900 if $cust_main->address2;
1902 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1903 push @address, $invoice_data{'country'}
1904 if $invoice_data{'country'};
1906 while (scalar(@address) < 5);
1908 $invoice_data{'logo_file'} = $params{'logo_file'}
1909 if $params{'logo_file'};
1911 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1912 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1913 #my $balance_due = $self->owed + $pr_total - $cr_total;
1914 my $balance_due = $self->owed + $pr_total;
1915 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1916 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1918 #do variable substitution in notes, footer, smallfooter
1919 foreach my $include (qw( notes footer smallfooter coupon )) {
1921 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1924 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1926 @inc_src = $conf->config($inc_file);
1930 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1932 my $convert_map = $convert_maps{$format}{$include};
1934 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1935 s/--\@\]/$delimiters{$format}[1]/g;
1938 &$convert_map( $conf->config($inc_file) );
1942 my $inc_tt = new Text::Template (
1944 SOURCE => [ map "$_\n", @inc_src ],
1945 DELIMITERS => $delimiters{$format},
1946 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1948 unless ( $inc_tt->compile() ) {
1949 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1950 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1954 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1956 $invoice_data{$include} =~ s/\n+$//
1957 if ($format eq 'latex');
1960 $invoice_data{'po_line'} =
1961 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1962 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
1965 my %money_chars = ( 'latex' => '',
1966 'html' => $conf->config('money_char') || '$',
1969 my $money_char = $money_chars{$format};
1971 my %other_money_chars = ( 'latex' => '\dollar ',
1972 'html' => $conf->config('money_char') || '$',
1975 my $other_money_char = $other_money_chars{$format};
1977 my @detail_items = ();
1978 my @total_items = ();
1982 $invoice_data{'detail_items'} = \@detail_items;
1983 $invoice_data{'total_items'} = \@total_items;
1984 $invoice_data{'buf'} = \@buf;
1985 $invoice_data{'sections'} = \@sections;
1987 my $previous_section = { 'description' => 'Previous Charges',
1988 'subtotal' => $other_money_char.
1989 sprintf('%.2f', $pr_total),
1993 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
1994 'subtotal' => $taxtotal }; # adjusted below
1996 my $adjusttotal = 0;
1997 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
1998 'subtotal' => 0 }; # adjusted below
2000 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2001 if ( $multisection ) {
2002 push @sections, $self->_items_sections;
2004 push @sections, { 'description' => '', 'subtotal' => '' };
2007 foreach my $line_item ( $conf->exists('disable_previous_balance')
2009 : $self->_items_previous
2013 ext_description => [],
2015 $detail->{'ref'} = $line_item->{'pkgnum'};
2016 $detail->{'quantity'} = 1;
2017 $detail->{'section'} = $previous_section;
2018 $detail->{'description'} = &$escape_function($line_item->{'description'});
2019 if ( exists $line_item->{'ext_description'} ) {
2020 @{$detail->{'ext_description'}} = map {
2021 &$escape_function($_);
2022 } @{$line_item->{'ext_description'}};
2024 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2025 $line_item->{'amount'};
2026 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2028 push @detail_items, $detail;
2029 push @buf, [ $detail->{'description'},
2030 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2034 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2035 push @buf, ['','-----------'];
2036 push @buf, [ 'Total Previous Balance',
2037 $money_char. sprintf("%10.2f", $pr_total) ];
2041 foreach my $section (@sections) {
2043 $section->{'subtotal'} = $other_money_char.
2044 sprintf('%.2f', $section->{'subtotal'})
2047 if ( $section->{'description'} ) {
2048 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2054 $options{'section'} = $section if $multisection;
2055 $options{'format'} = $format;
2056 $options{'escape_function'} = $escape_function;
2058 foreach my $line_item ( $self->_items_pkg(%options) ) {
2060 ext_description => [],
2062 $detail->{'ref'} = $line_item->{'pkgnum'};
2063 $detail->{'quantity'} = $line_item->{'quantity'};
2064 $detail->{'section'} = $section;
2065 $detail->{'description'} = &$escape_function($line_item->{'description'});
2066 if ( exists $line_item->{'ext_description'} ) {
2067 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2069 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2070 $line_item->{'amount'};
2071 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2072 $line_item->{'unit_amount'};
2073 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2075 push @detail_items, $detail;
2076 push @buf, ( [ $detail->{'description'},
2077 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2079 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2083 if ( $section->{'description'} ) {
2084 push @buf, ( ['','-----------'],
2085 [ $section->{'description'}. ' sub-total',
2086 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2095 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2096 unshift @sections, $previous_section if $pr_total;
2099 foreach my $tax ( $self->_items_tax ) {
2101 $total->{'total_item'} = &$escape_function($tax->{'description'});
2102 $taxtotal += $tax->{'amount'};
2103 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2104 if ( $multisection ) {
2105 my $money = $old_latex ? '' : $money_char;
2106 push @detail_items, {
2107 ext_description => [],
2110 description => &$escape_function($tax->{'description'}),
2111 amount => $money. $tax->{'amount'},
2113 section => $tax_section,
2116 push @total_items, $total;
2118 push @buf,[ $total->{'total_item'},
2119 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2126 $total->{'total_item'} = 'Sub-total';
2127 $total->{'total_amount'} =
2128 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2130 if ( $multisection ) {
2131 $tax_section->{'subtotal'} = $other_money_char.
2132 sprintf('%.2f', $taxtotal);
2133 $tax_section->{'pretotal'} = 'New charges sub-total '.
2134 $total->{'total_amount'};
2135 push @sections, $tax_section if $taxtotal;
2137 unshift @total_items, $total;
2140 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2142 push @buf,['','-----------'];
2143 push @buf,[( $conf->exists('disable_previous_balance')
2145 : 'Total New Charges'
2147 $money_char. sprintf("%10.2f",$self->charged) ];
2152 $total->{'total_item'} = &$embolden_function('Total');
2153 $total->{'total_amount'} =
2154 &$embolden_function(
2157 $self->charged + ( $conf->exists('disable_previous_balance')
2163 if ( $multisection ) {
2164 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2165 sprintf('%.2f', $self->charged );
2167 push @total_items, $total;
2169 push @buf,['','-----------'];
2170 push @buf,['Total Charges',
2172 sprintf( '%10.2f', $self->charged +
2173 ( $conf->exists('disable_previous_balance')
2182 unless ( $conf->exists('disable_previous_balance') ) {
2183 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2186 my $credittotal = 0;
2187 foreach my $credit ( $self->_items_credits ) {
2189 $total->{'total_item'} = &$escape_function($credit->{'description'});
2190 $credittotal += $credit->{'amount'};
2191 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2192 $adjusttotal += $credit->{'amount'};
2193 if ( $multisection ) {
2194 my $money = $old_latex ? '' : $money_char;
2195 push @detail_items, {
2196 ext_description => [],
2199 description => &$escape_function($credit->{'description'}),
2200 amount => $money. $credit->{'amount'},
2202 section => $adjust_section,
2205 push @total_items, $total;
2208 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2211 foreach ( $self->cust_credited ) {
2213 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2215 my $reason = substr($_->cust_credit->reason,0,32);
2216 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2217 $reason = " ($reason) " if $reason;
2219 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2220 $money_char. sprintf("%10.2f",$_->amount)
2225 my $paymenttotal = 0;
2226 foreach my $payment ( $self->_items_payments ) {
2228 $total->{'total_item'} = &$escape_function($payment->{'description'});
2229 $paymenttotal += $payment->{'amount'};
2230 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2231 $adjusttotal += $payment->{'amount'};
2232 if ( $multisection ) {
2233 my $money = $old_latex ? '' : $money_char;
2234 push @detail_items, {
2235 ext_description => [],
2238 description => &$escape_function($payment->{'description'}),
2239 amount => $money. $payment->{'amount'},
2241 section => $adjust_section,
2244 push @total_items, $total;
2246 push @buf, [ $payment->{'description'},
2247 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2250 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2252 if ( $multisection ) {
2253 $adjust_section->{'subtotal'} = $other_money_char.
2254 sprintf('%.2f', $adjusttotal);
2255 push @sections, $adjust_section;
2260 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2261 $total->{'total_amount'} =
2262 &$embolden_function(
2263 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2265 if ( $multisection ) {
2266 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2267 $total->{'total_amount'};
2269 push @total_items, $total;
2271 push @buf,['','-----------'];
2272 push @buf,[$self->balance_due_msg, $money_char.
2273 sprintf("%10.2f", $balance_due ) ];
2279 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2280 /invoice_lines\((\d*)\)/;
2281 $invoice_lines += $1 || scalar(@buf);
2284 die "no invoice_lines() functions in template?"
2285 if ( $format eq 'template' && !$wasfunc );
2287 if ($format eq 'template') {
2289 if ( $invoice_lines ) {
2290 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2291 $invoice_data{'total_pages'}++
2292 if scalar(@buf) % $invoice_lines;
2295 #setup subroutine for the template
2296 sub FS::cust_bill::_template::invoice_lines {
2297 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2299 scalar(@FS::cust_bill::_template::buf)
2300 ? shift @FS::cust_bill::_template::buf
2309 push @collect, split("\n",
2310 $text_template->fill_in( HASH => \%invoice_data,
2311 PACKAGE => 'FS::cust_bill::_template'
2314 $FS::cust_bill::_template::page++;
2316 map "$_\n", @collect;
2318 warn "filling in template for invoice ". $self->invnum. "\n"
2320 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2323 $text_template->fill_in(HASH => \%invoice_data);
2327 =item print_ps [ TIME [ , TEMPLATE ] ]
2329 Returns an postscript invoice, as a scalar.
2331 TIME an optional value used to control the printing of overdue messages. The
2332 default is now. It isn't the date of the invoice; that's the `_date' field.
2333 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2334 L<Time::Local> and L<Date::Parse> for conversion functions.
2341 my ($file, $lfile) = $self->print_latex(@_);
2342 my $ps = generate_ps($file);
2348 =item print_pdf [ TIME [ , TEMPLATE ] ]
2350 Returns an PDF invoice, as a scalar.
2352 TIME an optional value used to control the printing of overdue messages. The
2353 default is now. It isn't the date of the invoice; that's the `_date' field.
2354 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2355 L<Time::Local> and L<Date::Parse> for conversion functions.
2362 my ($file, $lfile) = $self->print_latex(@_);
2363 my $pdf = generate_pdf($file);
2369 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2371 Returns an HTML invoice, as a scalar.
2373 TIME an optional value used to control the printing of overdue messages. The
2374 default is now. It isn't the date of the invoice; that's the `_date' field.
2375 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2376 L<Time::Local> and L<Date::Parse> for conversion functions.
2378 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2379 when emailing the invoice as part of a multipart/related MIME email.
2384 my( $self, $today, $template, $cid ) = @_;
2386 my %params = ( 'format' => 'html' );
2387 $params{'time'} = $today if $today;
2388 $params{'template'} = $template if $template;
2389 $params{'cid'} = $cid if $cid;
2391 $self->print_generic( %params );
2394 # quick subroutine for print_latex
2396 # There are ten characters that LaTeX treats as special characters, which
2397 # means that they do not simply typeset themselves:
2398 # # $ % & ~ _ ^ \ { }
2400 # TeX ignores blanks following an escaped character; if you want a blank (as
2401 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2405 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2406 $value =~ s/([<>])/\$$1\$/g;
2410 #utility methods for print_*
2412 sub _translate_old_latex_format {
2413 warn "_translate_old_latex_format called\n"
2420 if ( $line =~ /^%%Detail\s*$/ ) {
2422 push @template, q![@--!,
2423 q! foreach my $_tr_line (@detail_items) {!,
2424 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2425 q! $_tr_line->{'description'} .= !,
2426 q! "\\tabularnewline\n~~".!,
2427 q! join( "\\tabularnewline\n~~",!,
2428 q! @{$_tr_line->{'ext_description'}}!,
2432 while ( ( my $line_item_line = shift )
2433 !~ /^%%EndDetail\s*$/ ) {
2434 $line_item_line =~ s/'/\\'/g; # nice LTS
2435 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2436 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2437 push @template, " \$OUT .= '$line_item_line';";
2440 push @template, '}',
2443 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2445 push @template, '[@--',
2446 ' foreach my $_tr_line (@total_items) {';
2448 while ( ( my $total_item_line = shift )
2449 !~ /^%%EndTotalDetails\s*$/ ) {
2450 $total_item_line =~ s/'/\\'/g; # nice LTS
2451 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2452 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2453 push @template, " \$OUT .= '$total_item_line';";
2456 push @template, '}',
2460 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2461 push @template, $line;
2467 warn "$_\n" foreach @template;
2476 #check for an invoice- specific override (eventually)
2478 #check for a customer- specific override
2479 return $self->cust_main->invoice_terms
2480 if $self->cust_main->invoice_terms;
2482 #use configured default or default default
2483 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2489 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2490 $duedate = $self->_date() + ( $1 * 86400 );
2497 $self->due_date ? time2str(shift, $self->due_date) : '';
2500 sub balance_due_msg {
2502 my $msg = 'Balance Due';
2503 return $msg unless $self->terms;
2504 if ( $self->due_date ) {
2505 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2506 } elsif ( $self->terms ) {
2507 $msg .= ' - '. $self->terms;
2512 sub balance_due_date {
2515 if ( $conf->exists('invoice_default_terms')
2516 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2517 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2522 =item invnum_date_pretty
2524 Returns a string with the invoice number and date, for example:
2525 "Invoice #54 (3/20/2008)"
2529 sub invnum_date_pretty {
2531 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2534 sub _items_sections {
2538 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2540 if ( $cust_bill_pkg->pkgnum > 0 ) {
2542 my $desc = $cust_bill_pkg->part_pkg->categoryname;
2544 $s{$desc} += $cust_bill_pkg->setup
2545 if ( $cust_bill_pkg->setup != 0 );
2547 $s{$desc} += $cust_bill_pkg->recur
2548 if ( $cust_bill_pkg->recur != 0 );
2554 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2561 #my @display = scalar(@_)
2563 # : qw( _items_previous _items_pkg );
2564 # #: qw( _items_pkg );
2565 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2566 my @display = qw( _items_previous _items_pkg );
2569 foreach my $display ( @display ) {
2570 push @b, $self->$display(@_);
2575 sub _items_previous {
2577 my $cust_main = $self->cust_main;
2578 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2580 foreach ( @pr_cust_bill ) {
2582 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2583 ' ('. time2str('%x',$_->_date). ')',
2584 #'pkgpart' => 'N/A',
2586 'amount' => sprintf("%.2f", $_->owed),
2592 # 'description' => 'Previous Balance',
2593 # #'pkgpart' => 'N/A',
2594 # 'pkgnum' => 'N/A',
2595 # 'amount' => sprintf("%10.2f", $pr_total ),
2596 # 'ext_description' => [ map {
2597 # "Invoice ". $_->invnum.
2598 # " (". time2str("%x",$_->_date). ") ".
2599 # sprintf("%10.2f", $_->owed)
2600 # } @pr_cust_bill ],
2608 my $section = delete $options{'section'};
2610 grep { $_->pkgnum &&
2612 ? $_->part_pkg->categoryname eq $section->{'description'}
2615 } $self->cust_bill_pkg;
2616 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2620 return 0 unless $a cmp $b;
2621 return -1 if $b eq 'Tax';
2622 return 1 if $a eq 'Tax';
2623 return -1 if $b eq 'Other surcharges';
2624 return 1 if $a eq 'Other surcharges';
2630 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2631 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2634 sub _items_cust_bill_pkg {
2636 my $cust_bill_pkg = shift;
2639 my $format = $opt{format} || '';
2640 my $escape_function = $opt{escape_function} || sub { shift };
2643 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2645 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2647 my $desc = $cust_bill_pkg->desc;
2649 my %details_opt = ( 'format' => $format,
2650 'escape_function' => $escape_function,
2653 if ( $cust_bill_pkg->pkgnum > 0 ) {
2655 if ( $cust_bill_pkg->setup != 0 ) {
2657 my $description = $desc;
2658 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2660 my @d = map &{$escape_function}($_),
2661 $cust_pkg->h_labels_short($self->_date);
2662 push @d, $cust_bill_pkg->details(%details_opt)
2663 if $cust_bill_pkg->recur == 0;
2666 description => $description,
2667 #pkgpart => $part_pkg->pkgpart,
2668 pkgnum => $cust_bill_pkg->pkgnum,
2669 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2670 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2671 quantity => $cust_bill_pkg->quantity,
2672 ext_description => \@d,
2676 if ( $cust_bill_pkg->recur != 0 ) {
2678 my $description = $desc;
2679 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2680 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2681 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2684 #at least until cust_bill_pkg has "past" ranges in addition to
2685 #the "future" sdate/edate ones... see #3032
2686 my @d = map &{$escape_function}($_),
2687 $cust_pkg->h_labels_short($self->_date);
2688 #$cust_bill_pkg->edate,
2689 #$cust_bill_pkg->sdate),
2690 @d = () if $cust_bill_pkg->itemdesc;
2691 push @d, $cust_bill_pkg->details(%details_opt);
2694 description => $description,
2695 #pkgpart => $part_pkg->pkgpart,
2696 pkgnum => $cust_bill_pkg->pkgnum,
2697 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2698 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2699 quantity => $cust_bill_pkg->quantity,
2700 ext_description => \@d,
2705 } else { #pkgnum tax or one-shot line item (??)
2707 if ( $cust_bill_pkg->setup != 0 ) {
2709 'description' => $desc,
2710 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2713 if ( $cust_bill_pkg->recur != 0 ) {
2715 'description' => "$desc (".
2716 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2717 time2str("%x", $cust_bill_pkg->edate). ')',
2718 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2730 sub _items_credits {
2735 foreach ( $self->cust_credited ) {
2737 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2739 my $reason = $_->cust_credit->reason;
2740 #my $reason = substr($_->cust_credit->reason,0,32);
2741 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2742 $reason = " ($reason) " if $reason;
2744 #'description' => 'Credit ref\#'. $_->crednum.
2745 # " (". time2str("%x",$_->cust_credit->_date) .")".
2747 'description' => 'Credit applied '.
2748 time2str("%x",$_->cust_credit->_date). $reason,
2749 'amount' => sprintf("%.2f",$_->amount),
2752 #foreach ( @cr_cust_credit ) {
2754 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2755 # $money_char. sprintf("%10.2f",$_->credited)
2763 sub _items_payments {
2767 #get & print payments
2768 foreach ( $self->cust_bill_pay ) {
2770 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2773 'description' => "Payment received ".
2774 time2str("%x",$_->cust_pay->_date ),
2775 'amount' => sprintf("%.2f", $_->amount )
2794 sub process_reprint {
2795 process_re_X('print', @_);
2802 sub process_reemail {
2803 process_re_X('email', @_);
2811 process_re_X('fax', @_);
2814 use Storable qw(thaw);
2818 my( $method, $job ) = ( shift, shift );
2819 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2821 my $param = thaw(decode_base64(shift));
2822 warn Dumper($param) if $DEBUG;
2833 my($method, $job, %param ) = @_;
2835 warn "re_X $method for job $job with param:\n".
2836 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2839 #some false laziness w/search/cust_bill.html
2841 my $orderby = 'ORDER BY cust_bill._date';
2843 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2845 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2847 my @cust_bill = qsearch( {
2848 #'select' => "cust_bill.*",
2849 'table' => 'cust_bill',
2850 'addl_from' => $addl_from,
2852 'extra_sql' => $extra_sql,
2853 'order_by' => $orderby,
2857 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2860 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2861 foreach my $cust_bill ( @cust_bill ) {
2862 $cust_bill->$method();
2864 if ( $job ) { #progressbar foo
2866 if ( time - $min_sec > $last ) {
2867 my $error = $job->update_statustext(
2868 int( 100 * $num / scalar(@cust_bill) )
2870 die $error if $error;
2881 =head1 CLASS METHODS
2887 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2893 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2898 Returns an SQL fragment to retreive the net amount (charged minus credited).
2904 'charged - '. $class->credited_sql;
2909 Returns an SQL fragment to retreive the amount paid against this invoice.
2915 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2916 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2921 Returns an SQL fragment to retreive the amount credited against this invoice.
2927 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2928 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2931 =item search_sql HASHREF
2933 Class method which returns an SQL WHERE fragment to search for parameters
2934 specified in HASHREF. Valid parameters are
2940 Epoch date (UNIX timestamp) setting a lower bound for _date values
2944 Epoch date (UNIX timestamp) setting an upper bound for _date values
2958 =item newest_percust
2962 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2967 my($class, $param) = @_;
2969 warn "$me search_sql called with params: \n".
2970 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2975 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2976 push @search, "cust_bill._date >= $1";
2978 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2979 push @search, "cust_bill._date < $1";
2981 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
2982 push @search, "cust_bill.invnum >= $1";
2984 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
2985 push @search, "cust_bill.invnum <= $1";
2987 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
2988 push @search, "cust_main.agentnum = $1";
2991 push @search, '0 != '. FS::cust_bill->owed_sql
2992 if $param->{'open'};
2994 push @search, '0 != '. FS::cust_bill->net_sql
2997 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
2998 if $param->{'days'};
3000 if ( $param->{'newest_percust'} ) {
3002 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3003 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3005 my @newest_where = map { my $x = $_;
3006 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3009 grep ! /^cust_main./, @search;
3010 my $newest_where = scalar(@newest_where)
3011 ? ' AND '. join(' AND ', @newest_where)
3015 push @search, "cust_bill._date = (
3016 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3017 WHERE newest_cust_bill.custnum = cust_bill.custnum
3023 my $curuser = $FS::CurrentUser::CurrentUser;
3024 if ( $curuser->username eq 'fs_queue'
3025 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3027 my $newuser = qsearchs('access_user', {
3028 'username' => $username,
3032 $curuser = $newuser;
3034 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3038 push @search, $curuser->agentnums_sql;
3040 join(' AND ', @search );
3052 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3053 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base