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;
20 use FS::cust_bill_pkg_display;
24 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
30 use FS::cust_bill_pay;
31 use FS::cust_bill_pay_batch;
32 use FS::part_bill_event;
35 @ISA = qw( FS::cust_main_Mixin FS::Record );
38 $me = '[FS::cust_bill]';
40 #ask FS::UID to run this stuff for us later
41 FS::UID->install_callback( sub {
43 $money_char = $conf->config('money_char') || '$';
48 FS::cust_bill - Object methods for cust_bill records
54 $record = new FS::cust_bill \%hash;
55 $record = new FS::cust_bill { 'column' => 'value' };
57 $error = $record->insert;
59 $error = $new_record->replace($old_record);
61 $error = $record->delete;
63 $error = $record->check;
65 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
67 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
69 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
71 @cust_pay_objects = $cust_bill->cust_pay;
73 $tax_amount = $record->tax;
75 @lines = $cust_bill->print_text;
76 @lines = $cust_bill->print_text $time;
80 An FS::cust_bill object represents an invoice; a declaration that a customer
81 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
82 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
83 following fields are currently supported:
87 =item invnum - primary key (assigned automatically for new invoices)
89 =item custnum - customer (see L<FS::cust_main>)
91 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
92 L<Time::Local> and L<Date::Parse> for conversion functions.
94 =item charged - amount of this invoice
96 =item printed - deprecated
98 =item closed - books closed flag, empty or `Y'
108 Creates a new invoice. To add the invoice to the database, see L<"insert">.
109 Invoices are normally created by calling the bill method of a customer object
110 (see L<FS::cust_main>).
114 sub table { 'cust_bill'; }
116 sub cust_linked { $_[0]->cust_main_custnum; }
117 sub cust_unlinked_msg {
119 "WARNING: can't find cust_main.custnum ". $self->custnum.
120 ' (cust_bill.invnum '. $self->invnum. ')';
125 Adds this invoice to the database ("Posts" the invoice). If there is an error,
126 returns the error, otherwise returns false.
130 This method now works but you probably shouldn't use it. Instead, apply a
131 credit against the invoice.
133 Using this method to delete invoices outright is really, really bad. There
134 would be no record you ever posted this invoice, and there are no check to
135 make sure charged = 0 or that there are no associated cust_bill_pkg records.
137 Really, don't use it.
143 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
144 $self->SUPER::delete(@_);
147 =item replace OLD_RECORD
149 Replaces the OLD_RECORD with this one in the database. If there is an error,
150 returns the error, otherwise returns false.
152 Only printed may be changed. printed is normally updated by calling the
153 collect method of a customer object (see L<FS::cust_main>).
157 #replace can be inherited from Record.pm
159 # replace_check is now the preferred way to #implement replace data checks
160 # (so $object->replace() works without an argument)
163 my( $new, $old ) = ( shift, shift );
164 return "Can't change custnum!" unless $old->custnum == $new->custnum;
165 #return "Can't change _date!" unless $old->_date eq $new->_date;
166 return "Can't change _date!" unless $old->_date == $new->_date;
167 return "Can't change charged!" unless $old->charged == $new->charged
168 || $old->charged == 0;
175 Checks all fields to make sure this is a valid invoice. If there is an error,
176 returns the error, otherwise returns false. Called by the insert and replace
185 $self->ut_numbern('invnum')
186 || $self->ut_number('custnum')
187 || $self->ut_numbern('_date')
188 || $self->ut_money('charged')
189 || $self->ut_numbern('printed')
190 || $self->ut_enum('closed', [ '', 'Y' ])
192 return $error if $error;
194 return "Unknown customer"
195 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
197 $self->_date(time) unless $self->_date;
199 $self->printed(0) if $self->printed eq '';
206 Returns a list consisting of the total previous balance for this customer,
207 followed by the previous outstanding invoices (as FS::cust_bill objects also).
214 my @cust_bill = sort { $a->_date <=> $b->_date }
215 grep { $_->owed != 0 && $_->_date < $self->_date }
216 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
218 foreach ( @cust_bill ) { $total += $_->owed; }
224 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
231 { 'table' => 'cust_bill_pkg',
232 'hashref' => { 'invnum' => $self->invnum },
233 'order_by' => 'ORDER BY billpkgnum',
240 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
247 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
249 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
252 =item open_cust_bill_pkg
254 Returns the open line items for this invoice.
256 Note that cust_bill_pkg with both setup and recur fees are returned as two
257 separate line items, each with only one fee.
261 # modeled after cust_main::open_cust_bill
262 sub open_cust_bill_pkg {
265 # grep { $_->owed > 0 } $self->cust_bill_pkg
267 my %other = ( 'recur' => 'setup',
268 'setup' => 'recur', );
270 foreach my $field ( qw( recur setup )) {
271 push @open, map { $_->set( $other{$field}, 0 ); $_; }
272 grep { $_->owed($field) > 0 }
273 $self->cust_bill_pkg;
279 =item cust_bill_event
281 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
285 sub cust_bill_event {
287 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
290 =item num_cust_bill_event
292 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
296 sub num_cust_bill_event {
299 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
300 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
301 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
302 $sth->fetchrow_arrayref->[0];
307 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
311 #false laziness w/cust_pkg.pm
315 'table' => 'cust_event',
316 'addl_from' => 'JOIN part_event USING ( eventpart )',
317 'hashref' => { 'tablenum' => $self->invnum },
318 'extra_sql' => " AND eventtable = 'cust_bill' ",
324 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
328 #false laziness w/cust_pkg.pm
332 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
333 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
334 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
335 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
336 $sth->fetchrow_arrayref->[0];
341 Returns the customer (see L<FS::cust_main>) for this invoice.
347 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
350 =item cust_suspend_if_balance_over AMOUNT
352 Suspends the customer associated with this invoice if the total amount owed on
353 this invoice and all older invoices is greater than the specified amount.
355 Returns a list: an empty list on success or a list of errors.
359 sub cust_suspend_if_balance_over {
360 my( $self, $amount ) = ( shift, shift );
361 my $cust_main = $self->cust_main;
362 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
365 $cust_main->suspend(@_);
371 Depreciated. See the cust_credited method.
373 #Returns a list consisting of the total previous credited (see
374 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
375 #outstanding credits (FS::cust_credit objects).
381 croak "FS::cust_bill->cust_credit depreciated; see ".
382 "FS::cust_bill->cust_credit_bill";
385 #my @cust_credit = sort { $a->_date <=> $b->_date }
386 # grep { $_->credited != 0 && $_->_date < $self->_date }
387 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
389 #foreach (@cust_credit) { $total += $_->credited; }
390 #$total, @cust_credit;
395 Depreciated. See the cust_bill_pay method.
397 #Returns all payments (see L<FS::cust_pay>) for this invoice.
403 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
405 #sort { $a->_date <=> $b->_date }
406 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
412 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
418 sort { $a->_date <=> $b->_date }
419 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
424 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
430 sort { $a->_date <=> $b->_date }
431 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
437 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
444 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
446 foreach (@taxlines) { $total += $_->setup; }
452 Returns the amount owed (still outstanding) on this invoice, which is charged
453 minus all payment applications (see L<FS::cust_bill_pay>) and credit
454 applications (see L<FS::cust_credit_bill>).
460 my $balance = $self->charged;
461 $balance -= $_->amount foreach ( $self->cust_bill_pay );
462 $balance -= $_->amount foreach ( $self->cust_credited );
463 $balance = sprintf( "%.2f", $balance);
464 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
468 =item apply_payments_and_credits
472 sub apply_payments_and_credits {
475 local $SIG{HUP} = 'IGNORE';
476 local $SIG{INT} = 'IGNORE';
477 local $SIG{QUIT} = 'IGNORE';
478 local $SIG{TERM} = 'IGNORE';
479 local $SIG{TSTP} = 'IGNORE';
480 local $SIG{PIPE} = 'IGNORE';
482 my $oldAutoCommit = $FS::UID::AutoCommit;
483 local $FS::UID::AutoCommit = 0;
486 $self->select_for_update; #mutex
488 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
489 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
491 while ( $self->owed > 0 and ( @payments || @credits ) ) {
494 if ( @payments && @credits ) {
496 #decide which goes first by weight of top (unapplied) line item
498 my @open_lineitems = $self->open_cust_bill_pkg;
501 max( map { $_->part_pkg->pay_weight || 0 }
506 my $max_credit_weight =
507 max( map { $_->part_pkg->credit_weight || 0 }
513 #if both are the same... payments first? it has to be something
514 if ( $max_pay_weight >= $max_credit_weight ) {
520 } elsif ( @payments ) {
522 } elsif ( @credits ) {
525 die "guru meditation #12 and 35";
528 if ( $app eq 'pay' ) {
530 my $payment = shift @payments;
532 $app = new FS::cust_bill_pay {
533 'paynum' => $payment->paynum,
534 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
537 } elsif ( $app eq 'credit' ) {
539 my $credit = shift @credits;
541 $app = new FS::cust_credit_bill {
542 'crednum' => $credit->crednum,
543 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
547 die "guru meditation #12 and 35";
550 $app->invnum( $self->invnum );
552 my $error = $app->insert;
554 $dbh->rollback if $oldAutoCommit;
555 return "Error inserting ". $app->table. " record: $error";
557 die $error if $error;
561 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
566 =item generate_email OPTION => VALUE ...
574 sender address, required
578 alternate template name, optional
582 text attachment arrayref, optional
586 email subject, optional
590 Returns an argument list to be passed to L<FS::Misc::send_email>.
601 my $me = '[FS::cust_bill::generate_email]';
604 'from' => $args{'from'},
605 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
608 if (ref($args{'to'}) eq 'ARRAY') {
609 $return{'to'} = $args{'to'};
611 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
612 $self->cust_main->invoicing_list
616 if ( $conf->exists('invoice_html') ) {
618 warn "$me creating HTML/text multipart message"
621 $return{'nobody'} = 1;
623 my $alternative = build MIME::Entity
624 'Type' => 'multipart/alternative',
625 'Encoding' => '7bit',
626 'Disposition' => 'inline'
630 if ( $conf->exists('invoice_email_pdf')
631 and scalar($conf->config('invoice_email_pdf_note')) ) {
633 warn "$me using 'invoice_email_pdf_note' in multipart message"
635 $data = [ map { $_ . "\n" }
636 $conf->config('invoice_email_pdf_note')
641 warn "$me not using 'invoice_email_pdf_note' in multipart message"
643 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
644 $data = $args{'print_text'};
646 $data = [ $self->print_text('', $args{'template'}) ];
651 $alternative->attach(
652 'Type' => 'text/plain',
653 #'Encoding' => 'quoted-printable',
654 'Encoding' => '7bit',
656 'Disposition' => 'inline',
659 $args{'from'} =~ /\@([\w\.\-]+)/;
660 my $from = $1 || 'example.com';
661 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
663 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
665 if ( defined($args{'template'}) && length($args{'template'})
666 && -e "$path/logo_". $args{'template'}. ".png"
669 $file = "$path/logo_". $args{'template'}. ".png";
671 $file = "$path/logo.png";
674 my $image = build MIME::Entity
675 'Type' => 'image/png',
676 'Encoding' => 'base64',
678 'Filename' => 'logo.png',
679 'Content-ID' => "<$content_id>",
682 $alternative->attach(
683 'Type' => 'text/html',
684 'Encoding' => 'quoted-printable',
685 'Data' => [ '<html>',
688 ' '. encode_entities($return{'subject'}),
691 ' <body bgcolor="#e8e8e8">',
692 $self->print_html('', $args{'template'}, $content_id),
696 'Disposition' => 'inline',
697 #'Filename' => 'invoice.pdf',
700 if ( $conf->exists('invoice_email_pdf') ) {
705 # multipart/alternative
711 my $related = build MIME::Entity 'Type' => 'multipart/related',
712 'Encoding' => '7bit';
714 #false laziness w/Misc::send_email
715 $related->head->replace('Content-type',
717 '; boundary="'. $related->head->multipart_boundary. '"'.
718 '; type=multipart/alternative'
721 $related->add_part($alternative);
723 $related->add_part($image);
725 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
727 $return{'mimeparts'} = [ $related, $pdf ];
731 #no other attachment:
733 # multipart/alternative
738 $return{'content-type'} = 'multipart/related';
739 $return{'mimeparts'} = [ $alternative, $image ];
740 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
741 #$return{'disposition'} = 'inline';
747 if ( $conf->exists('invoice_email_pdf') ) {
748 warn "$me creating PDF attachment"
751 #mime parts arguments a la MIME::Entity->build().
752 $return{'mimeparts'} = [
753 { $self->mimebuild_pdf('', $args{'template'}) }
757 if ( $conf->exists('invoice_email_pdf')
758 and scalar($conf->config('invoice_email_pdf_note')) ) {
760 warn "$me using 'invoice_email_pdf_note'"
762 $return{'body'} = [ map { $_ . "\n" }
763 $conf->config('invoice_email_pdf_note')
768 warn "$me not using 'invoice_email_pdf_note'"
770 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
771 $return{'body'} = $args{'print_text'};
773 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
786 Returns a list suitable for passing to MIME::Entity->build(), representing
787 this invoice as PDF attachment.
794 'Type' => 'application/pdf',
795 'Encoding' => 'base64',
796 'Data' => [ $self->print_pdf(@_) ],
797 'Disposition' => 'attachment',
798 'Filename' => 'invoice.pdf',
802 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
804 Sends this invoice to the destinations configured for this customer: sends
805 email, prints and/or faxes. See L<FS::cust_main_invoice>.
807 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
809 AGENTNUM, if specified, means that this invoice will only be sent for customers
810 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
811 single agent) or an arrayref of agentnums.
813 INVOICE_FROM, if specified, overrides the default email invoice From: address.
815 AMOUNT, if specified, only sends the invoice if the total amount owed on this
816 invoice and all older invoices is greater than the specified amount.
823 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
824 or die "invalid invoice number: " . $opt{invnum};
826 my @args = ( $opt{template}, $opt{agentnum} );
827 push @args, $opt{invoice_from}
828 if exists($opt{invoice_from}) && $opt{invoice_from};
830 my $error = $self->send( @args );
831 die $error if $error;
837 my $template = scalar(@_) ? shift : '';
838 if ( scalar(@_) && $_[0] ) {
839 my $agentnums = ref($_[0]) ? shift : [ shift ];
840 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
846 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
848 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
851 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
853 my @invoicing_list = $self->cust_main->invoicing_list;
855 #$self->email_invoice($template, $invoice_from)
856 $self->email($template, $invoice_from)
857 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
859 #$self->print_invoice($template)
860 $self->print($template)
861 if grep { $_ eq 'POST' } @invoicing_list; #postal
863 $self->fax_invoice($template)
864 if grep { $_ eq 'FAX' } @invoicing_list; #fax
870 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
874 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
876 INVOICE_FROM, if specified, overrides the default email invoice From: address.
880 sub queueable_email {
883 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
884 or die "invalid invoice number: " . $opt{invnum};
886 my @args = ( $opt{template} );
887 push @args, $opt{invoice_from}
888 if exists($opt{invoice_from}) && $opt{invoice_from};
890 my $error = $self->email( @args );
891 die $error if $error;
898 my $template = scalar(@_) ? shift : '';
902 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
904 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
905 $self->cust_main->invoicing_list;
907 #better to notify this person than silence
908 @invoicing_list = ($invoice_from) unless @invoicing_list;
910 my $error = send_email(
911 $self->generate_email(
912 'from' => $invoice_from,
913 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
914 'template' => $template,
917 die "can't email invoice: $error\n" if $error;
918 #die "$error\n" if $error;
922 =item lpr_data [ TEMPLATENAME ]
924 Returns the postscript or plaintext for this invoice as an arrayref.
926 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
931 my( $self, $template) = @_;
932 $conf->exists('invoice_latex')
933 ? [ $self->print_ps('', $template) ]
934 : [ $self->print_text('', $template) ];
937 =item print [ TEMPLATENAME ]
941 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
948 my $template = scalar(@_) ? shift : '';
950 do_print $self->lpr_data($template);
953 =item fax_invoice [ TEMPLATENAME ]
957 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
963 my $template = scalar(@_) ? shift : '';
965 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
966 unless $conf->exists('invoice_latex');
968 my $dialstring = $self->cust_main->getfield('fax');
971 my $error = send_fax( 'docdata' => $self->lpr_data($template),
972 'dialstring' => $dialstring,
974 die $error if $error;
978 =item ftp_invoice [ TEMPLATENAME ]
980 Sends this invoice data via FTP.
982 TEMPLATENAME is unused?
988 my $template = scalar(@_) ? shift : '';
992 'server' => $conf->config('cust_bill-ftpserver'),
993 'username' => $conf->config('cust_bill-ftpusername'),
994 'password' => $conf->config('cust_bill-ftppassword'),
995 'dir' => $conf->config('cust_bill-ftpdir'),
996 'format' => $conf->config('cust_bill-ftpformat'),
1000 =item spool_invoice [ TEMPLATENAME ]
1002 Spools this invoice data (see L<FS::spool_csv>)
1004 TEMPLATENAME is unused?
1010 my $template = scalar(@_) ? shift : '';
1013 'format' => $conf->config('cust_bill-spoolformat'),
1014 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1018 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1020 Like B<send>, but only sends the invoice if it is the newest open invoice for
1025 sub send_if_newest {
1030 grep { $_->owed > 0 }
1031 qsearch('cust_bill', {
1032 'custnum' => $self->custnum,
1033 #'_date' => { op=>'>', value=>$self->_date },
1034 'invnum' => { op=>'>', value=>$self->invnum },
1041 =item send_csv OPTION => VALUE, ...
1043 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1047 protocol - currently only "ftp"
1053 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1054 and YYMMDDHHMMSS is a timestamp.
1056 See L</print_csv> for a description of the output format.
1061 my($self, %opt) = @_;
1065 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1066 mkdir $spooldir, 0700 unless -d $spooldir;
1068 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1069 my $file = "$spooldir/$tracctnum.csv";
1071 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1073 open(CSV, ">$file") or die "can't open $file: $!";
1081 if ( $opt{protocol} eq 'ftp' ) {
1082 eval "use Net::FTP;";
1084 $net = Net::FTP->new($opt{server}) or die @$;
1086 die "unknown protocol: $opt{protocol}";
1089 $net->login( $opt{username}, $opt{password} )
1090 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1092 $net->binary or die "can't set binary mode";
1094 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1096 $net->put($file) or die "can't put $file: $!";
1106 Spools CSV invoice data.
1112 =item format - 'default' or 'billco'
1114 =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>).
1116 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1118 =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.
1125 my($self, %opt) = @_;
1127 my $cust_main = $self->cust_main;
1129 if ( $opt{'dest'} ) {
1130 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1131 $cust_main->invoicing_list;
1132 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1133 || ! keys %invoicing_list;
1136 if ( $opt{'balanceover'} ) {
1138 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1141 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1142 mkdir $spooldir, 0700 unless -d $spooldir;
1144 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1148 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1149 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1152 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1154 open(CSV, ">>$file") or die "can't open $file: $!";
1155 flock(CSV, LOCK_EX);
1160 if ( lc($opt{'format'}) eq 'billco' ) {
1162 flock(CSV, LOCK_UN);
1167 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1170 open(CSV,">>$file") or die "can't open $file: $!";
1171 flock(CSV, LOCK_EX);
1177 flock(CSV, LOCK_UN);
1184 =item print_csv OPTION => VALUE, ...
1186 Returns CSV data for this invoice.
1190 format - 'default' or 'billco'
1192 Returns a list consisting of two scalars. The first is a single line of CSV
1193 header information for this invoice. The second is one or more lines of CSV
1194 detail information for this invoice.
1196 If I<format> is not specified or "default", the fields of the CSV file are as
1199 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1203 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1205 B<record_type> is C<cust_bill> for the initial header line only. The
1206 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1207 fields are filled in.
1209 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1210 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1213 =item invnum - invoice number
1215 =item custnum - customer number
1217 =item _date - invoice date
1219 =item charged - total invoice amount
1221 =item first - customer first name
1223 =item last - customer first name
1225 =item company - company name
1227 =item address1 - address line 1
1229 =item address2 - address line 1
1239 =item pkg - line item description
1241 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1243 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1245 =item sdate - start date for recurring fee
1247 =item edate - end date for recurring fee
1251 If I<format> is "billco", the fields of the header CSV file are as follows:
1253 +-------------------------------------------------------------------+
1254 | FORMAT HEADER FILE |
1255 |-------------------------------------------------------------------|
1256 | Field | Description | Name | Type | Width |
1257 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1258 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1259 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1260 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1261 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1262 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1263 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1264 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1265 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1266 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1267 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1268 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1269 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1270 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1271 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1272 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1273 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1274 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1275 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1276 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1277 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1278 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1279 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1280 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1281 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1282 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1283 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1284 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1285 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1286 +-------+-------------------------------+------------+------+-------+
1288 If I<format> is "billco", the fields of the detail CSV file are as follows:
1290 FORMAT FOR DETAIL FILE
1292 Field | Description | Name | Type | Width
1293 1 | N/A-Leave Empty | RC | CHAR | 2
1294 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1295 3 | Account Number | TRACCTNUM | CHAR | 15
1296 4 | Invoice Number | TRINVOICE | CHAR | 15
1297 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1298 6 | Transaction Detail | DETAILS | CHAR | 100
1299 7 | Amount | AMT | NUM* | 9
1300 8 | Line Format Control** | LNCTRL | CHAR | 2
1301 9 | Grouping Code | GROUP | CHAR | 2
1302 10 | User Defined | ACCT CODE | CHAR | 15
1307 my($self, %opt) = @_;
1309 eval "use Text::CSV_XS";
1312 my $cust_main = $self->cust_main;
1314 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1316 if ( lc($opt{'format'}) eq 'billco' ) {
1319 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1321 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1323 my( $previous_balance, @unused ) = $self->previous; #previous balance
1325 my $pmt_cr_applied = 0;
1326 $pmt_cr_applied += $_->{'amount'}
1327 foreach ( $self->_items_payments, $self->_items_credits ) ;
1329 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1332 '', # 1 | N/A-Leave Empty CHAR 2
1333 '', # 2 | N/A-Leave Empty CHAR 15
1334 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1335 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1336 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1337 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1338 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1339 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1340 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1341 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1342 '', # 10 | Ancillary Billing Information CHAR 30
1343 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1344 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1347 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1350 $duedate, # 14 | Bill Due Date CHAR 10
1352 $previous_balance, # 15 | Previous Balance NUM* 9
1353 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1354 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1355 $totaldue, # 18 | Total Amt Due NUM* 9
1356 $totaldue, # 19 | Total Amt Due NUM* 9
1357 '', # 20 | 30 Day Aging NUM* 9
1358 '', # 21 | 60 Day Aging NUM* 9
1359 '', # 22 | 90 Day Aging NUM* 9
1360 'N', # 23 | Y/N CHAR 1
1361 '', # 24 | Remittance automation CHAR 100
1362 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1363 $self->custnum, # 26 | Customer Reference Number CHAR 15
1364 '0', # 27 | Federal Tax*** NUM* 9
1365 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1366 '0', # 29 | Other Taxes & Fees*** NUM* 9
1375 time2str("%x", $self->_date),
1376 sprintf("%.2f", $self->charged),
1377 ( map { $cust_main->getfield($_) }
1378 qw( first last company address1 address2 city state zip country ) ),
1380 ) or die "can't create csv";
1383 my $header = $csv->string. "\n";
1386 if ( lc($opt{'format'}) eq 'billco' ) {
1389 foreach my $item ( $self->_items_pkg ) {
1392 '', # 1 | N/A-Leave Empty CHAR 2
1393 '', # 2 | N/A-Leave Empty CHAR 15
1394 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1395 $self->invnum, # 4 | Invoice Number CHAR 15
1396 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1397 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1398 $item->{'amount'}, # 7 | Amount NUM* 9
1399 '', # 8 | Line Format Control** CHAR 2
1400 '', # 9 | Grouping Code CHAR 2
1401 '', # 10 | User Defined CHAR 15
1404 $detail .= $csv->string. "\n";
1410 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1412 my($pkg, $setup, $recur, $sdate, $edate);
1413 if ( $cust_bill_pkg->pkgnum ) {
1415 ($pkg, $setup, $recur, $sdate, $edate) = (
1416 $cust_bill_pkg->part_pkg->pkg,
1417 ( $cust_bill_pkg->setup != 0
1418 ? sprintf("%.2f", $cust_bill_pkg->setup )
1420 ( $cust_bill_pkg->recur != 0
1421 ? sprintf("%.2f", $cust_bill_pkg->recur )
1423 ( $cust_bill_pkg->sdate
1424 ? time2str("%x", $cust_bill_pkg->sdate)
1426 ($cust_bill_pkg->edate
1427 ?time2str("%x", $cust_bill_pkg->edate)
1431 } else { #pkgnum tax
1432 next unless $cust_bill_pkg->setup != 0;
1433 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1434 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1436 ($pkg, $setup, $recur, $sdate, $edate) =
1437 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1443 ( map { '' } (1..11) ),
1444 ($pkg, $setup, $recur, $sdate, $edate)
1445 ) or die "can't create csv";
1447 $detail .= $csv->string. "\n";
1453 ( $header, $detail );
1459 Pays this invoice with a compliemntary payment. If there is an error,
1460 returns the error, otherwise returns false.
1466 my $cust_pay = new FS::cust_pay ( {
1467 'invnum' => $self->invnum,
1468 'paid' => $self->owed,
1471 'payinfo' => $self->cust_main->payinfo,
1479 Attempts to pay this invoice with a credit card payment via a
1480 Business::OnlinePayment realtime gateway. See
1481 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1482 for supported processors.
1488 $self->realtime_bop( 'CC', @_ );
1493 Attempts to pay this invoice with an electronic check (ACH) payment via a
1494 Business::OnlinePayment realtime gateway. See
1495 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1496 for supported processors.
1502 $self->realtime_bop( 'ECHECK', @_ );
1507 Attempts to pay this invoice with phone bill (LEC) payment via a
1508 Business::OnlinePayment realtime gateway. See
1509 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1510 for supported processors.
1516 $self->realtime_bop( 'LEC', @_ );
1520 my( $self, $method ) = @_;
1522 my $cust_main = $self->cust_main;
1523 my $balance = $cust_main->balance;
1524 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1525 $amount = sprintf("%.2f", $amount);
1526 return "not run (balance $balance)" unless $amount > 0;
1528 my $description = 'Internet Services';
1529 if ( $conf->exists('business-onlinepayment-description') ) {
1530 my $dtempl = $conf->config('business-onlinepayment-description');
1532 my $agent_obj = $cust_main->agent
1533 or die "can't retreive agent for $cust_main (agentnum ".
1534 $cust_main->agentnum. ")";
1535 my $agent = $agent_obj->agent;
1536 my $pkgs = join(', ',
1537 map { $_->part_pkg->pkg }
1538 grep { $_->pkgnum } $self->cust_bill_pkg
1540 $description = eval qq("$dtempl");
1543 $cust_main->realtime_bop($method, $amount,
1544 'description' => $description,
1545 'invnum' => $self->invnum,
1550 =item batch_card OPTION => VALUE...
1552 Adds a payment for this invoice to the pending credit card batch (see
1553 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1554 runs the payment using a realtime gateway.
1559 my ($self, %options) = @_;
1560 my $cust_main = $self->cust_main;
1562 $options{invnum} = $self->invnum;
1564 $cust_main->batch_card(%options);
1567 sub _agent_template {
1569 $self->cust_main->agent_template;
1572 sub _agent_invoice_from {
1574 $self->cust_main->agent_invoice_from;
1577 =item print_text [ TIME [ , TEMPLATE ] ]
1579 Returns an text invoice, as a list of lines.
1581 TIME an optional value used to control the printing of overdue messages. The
1582 default is now. It isn't the date of the invoice; that's the `_date' field.
1583 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1584 L<Time::Local> and L<Date::Parse> for conversion functions.
1589 my( $self, $today, $template ) = @_;
1591 my %params = ( 'format' => 'template' );
1592 $params{'time'} = $today if $today;
1593 $params{'template'} = $template if $template;
1595 $self->print_generic( %params );
1598 =item print_latex [ TIME [ , TEMPLATE ] ]
1600 Internal method - returns a filename of a filled-in LaTeX template for this
1601 invoice (Note: add ".tex" to get the actual filename), and a filename of
1602 an associated logo (with the .eps extension included).
1604 See print_ps and print_pdf for methods that return PostScript and PDF output.
1606 TIME an optional value used to control the printing of overdue messages. The
1607 default is now. It isn't the date of the invoice; that's the `_date' field.
1608 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1609 L<Time::Local> and L<Date::Parse> for conversion functions.
1615 my( $self, $today, $template ) = @_;
1617 my %params = ( 'format' => 'latex' );
1618 $params{'time'} = $today if $today;
1619 $params{'template'} = $template if $template;
1621 $template ||= $self->_agent_template;
1623 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1624 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1628 ) or die "can't open temp file: $!\n";
1630 if ($template && $conf->exists("logo_${template}.eps")) {
1631 print $lh $conf->config_binary("logo_${template}.eps")
1632 or die "can't write temp file: $!\n";
1634 print $lh $conf->config_binary('logo.eps')
1635 or die "can't write temp file: $!\n";
1638 $params{'logo_file'} = $lh->filename;
1640 my @filled_in = $self->print_generic( %params );
1642 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1646 ) or die "can't open temp file: $!\n";
1647 print $fh join('', @filled_in );
1650 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1651 return ($1, $params{'logo_file'});
1655 =item print_generic OPTIONS_HASH
1657 Internal method - returns a filled-in template for this invoice as a scalar.
1659 See print_ps and print_pdf for methods that return PostScript and PDF output.
1661 Non optional options include
1662 format - latex, html, template
1664 Optional options include
1666 template - a value used as a suffix for a configuration template
1668 time - a value used to control the printing of overdue messages. The
1669 default is now. It isn't the date of the invoice; that's the `_date' field.
1670 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1671 L<Time::Local> and L<Date::Parse> for conversion functions.
1675 unsquelch_cdr - overrides any per customer cdr squelching when true
1681 my( $self, %params ) = @_;
1682 my $today = $params{today} ? $params{today} : time;
1683 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1686 my $format = $params{format};
1687 die "Unknown format: $format"
1688 unless $format =~ /^(latex|html|template)$/;
1690 my $cust_main = $self->cust_main;
1691 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1692 unless $cust_main->payname
1693 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1695 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1696 'html' => [ '<%=', '%>' ],
1697 'template' => [ '{', '}' ],
1700 #create the template
1701 my $template = $params{template} ? $params{template} : $self->_agent_template;
1702 my $templatefile = "invoice_$format";
1703 $templatefile .= "_$template"
1704 if length($template);
1705 my @invoice_template = map "$_\n", $conf->config($templatefile)
1706 or die "cannot load config data $templatefile";
1709 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1710 #change this to a die when the old code is removed
1711 warn "old-style invoice template $templatefile; ".
1712 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1713 $old_latex = 'true';
1714 @invoice_template = _translate_old_latex_format(@invoice_template);
1717 my $text_template = new Text::Template(
1719 SOURCE => \@invoice_template,
1720 DELIMITERS => $delimiters{$format},
1723 $text_template->compile()
1724 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1727 # additional substitution could possibly cause breakage in existing templates
1728 my %convert_maps = (
1730 'notes' => sub { map "$_", @_ },
1731 'footer' => sub { map "$_", @_ },
1732 'smallfooter' => sub { map "$_", @_ },
1733 'returnaddress' => sub { map "$_", @_ },
1734 'coupon' => sub { map "$_", @_ },
1740 s/%%(.*)$/<!-- $1 -->/g;
1741 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1742 s/\\begin\{enumerate\}/<ol>/g;
1744 s/\\end\{enumerate\}/<\/ol>/g;
1745 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1754 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1756 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1761 s/\\\\\*?\s*$/<BR>/;
1762 s/\\hyphenation\{[\w\s\-]+}//;
1766 'coupon' => sub { "" },
1773 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1774 s/\\begin\{enumerate\}//g;
1776 s/\\end\{enumerate\}//g;
1777 s/\\textbf\{(.*)\}/$1/g;
1784 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1786 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1791 s/\\\\\*?\s*$/\n/; # dubious
1792 s/\\hyphenation\{[\w\s\-]+}//;
1796 'coupon' => sub { "" },
1801 # hashes for differing output formats
1802 my %nbsps = ( 'latex' => '~',
1803 'html' => '', # '&nbps;' would be nice
1804 'template' => '', # not used
1806 my $nbsp = $nbsps{$format};
1808 my %escape_functions = ( 'latex' => \&_latex_escape,
1809 'html' => \&encode_entities,
1810 'template' => sub { shift },
1812 my $escape_function = $escape_functions{$format};
1814 my %date_formats = ( 'latex' => '%b %o, %Y',
1815 'html' => '%b %o, %Y',
1818 my $date_format = $date_formats{$format};
1820 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1822 'html' => sub { return '<b>'. shift(). '</b>'
1824 'template' => sub { shift },
1826 my $embolden_function = $embolden_functions{$format};
1829 # generate template variables
1832 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1836 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1842 $returnaddress = join("\n",
1843 $conf->config_orbase("invoice_${format}returnaddress", $template)
1846 } elsif ( grep /\S/,
1847 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1849 my $convert_map = $convert_maps{$format}{'returnaddress'};
1852 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1857 } elsif ( grep /\S/, $conf->config('company_address') ) {
1859 my $convert_map = $convert_maps{$format}{'returnaddress'};
1860 $returnaddress = join( "\n", &$convert_map(
1861 map { s/( {2,})/'~' x length($1)/eg;
1865 ( $conf->config('company_name'),
1866 $conf->config('company_address'),
1873 my $warning = "Couldn't find a return address; ".
1874 "do you need to set the company_address configuration value?";
1876 $returnaddress = $nbsp;
1877 #$returnaddress = $warning;
1881 my %invoice_data = (
1882 'company_name' => scalar( $conf->config('company_name') ),
1883 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1884 'custnum' => $cust_main->display_custnum,
1885 'invnum' => $self->invnum,
1886 'date' => time2str($date_format, $self->_date),
1887 'today' => time2str('%b %o, %Y', $today),
1888 'agent' => &$escape_function($cust_main->agent->agent),
1889 'agent_custid' => &$escape_function($cust_main->agent_custid),
1890 'payname' => &$escape_function($cust_main->payname),
1891 'company' => &$escape_function($cust_main->company),
1892 'address1' => &$escape_function($cust_main->address1),
1893 'address2' => &$escape_function($cust_main->address2),
1894 'city' => &$escape_function($cust_main->city),
1895 'state' => &$escape_function($cust_main->state),
1896 'zip' => &$escape_function($cust_main->zip),
1897 'fax' => &$escape_function($cust_main->fax),
1898 'returnaddress' => $returnaddress,
1900 'terms' => $self->terms,
1901 'template' => $params{'template'},
1902 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1903 # better hang on to conf_dir for a while
1904 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1907 'current_charges' => sprintf("%.2f", $self->charged),
1908 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1909 'ship_enable' => $conf->exists('invoice-ship_address'),
1910 'unitprices' => $conf->exists('invoice-unitprice'),
1913 my $countrydefault = $conf->config('countrydefault') || 'US';
1914 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1915 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1916 my $method = $prefix.$_;
1917 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1919 $invoice_data{'ship_country'} = ''
1920 if ( $invoice_data{'ship_country'} eq $countrydefault );
1922 $invoice_data{'cid'} = $params{'cid'}
1925 if ( $cust_main->country eq $countrydefault ) {
1926 $invoice_data{'country'} = '';
1928 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1932 $invoice_data{'address'} = \@address;
1934 $cust_main->payname.
1935 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1936 ? " (P.O. #". $cust_main->payinfo. ")"
1940 push @address, $cust_main->company
1941 if $cust_main->company;
1942 push @address, $cust_main->address1;
1943 push @address, $cust_main->address2
1944 if $cust_main->address2;
1946 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1947 push @address, $invoice_data{'country'}
1948 if $invoice_data{'country'};
1950 while (scalar(@address) < 5);
1952 $invoice_data{'logo_file'} = $params{'logo_file'}
1953 if $params{'logo_file'};
1955 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1956 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1957 #my $balance_due = $self->owed + $pr_total - $cr_total;
1958 my $balance_due = $self->owed + $pr_total;
1959 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1960 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1962 #do variable substitution in notes, footer, smallfooter
1963 foreach my $include (qw( notes footer smallfooter coupon )) {
1965 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1968 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1970 @inc_src = $conf->config($inc_file);
1974 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1976 my $convert_map = $convert_maps{$format}{$include};
1978 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1979 s/--\@\]/$delimiters{$format}[1]/g;
1982 &$convert_map( $conf->config($inc_file) );
1986 my $inc_tt = new Text::Template (
1988 SOURCE => [ map "$_\n", @inc_src ],
1989 DELIMITERS => $delimiters{$format},
1990 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1992 unless ( $inc_tt->compile() ) {
1993 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1994 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1998 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2000 $invoice_data{$include} =~ s/\n+$//
2001 if ($format eq 'latex');
2004 $invoice_data{'po_line'} =
2005 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2006 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2009 my %money_chars = ( 'latex' => '',
2010 'html' => $conf->config('money_char') || '$',
2013 my $money_char = $money_chars{$format};
2015 my %other_money_chars = ( 'latex' => '\dollar ',
2016 'html' => $conf->config('money_char') || '$',
2019 my $other_money_char = $other_money_chars{$format};
2021 my @detail_items = ();
2022 my @total_items = ();
2026 $invoice_data{'detail_items'} = \@detail_items;
2027 $invoice_data{'total_items'} = \@total_items;
2028 $invoice_data{'buf'} = \@buf;
2029 $invoice_data{'sections'} = \@sections;
2031 my $previous_section = { 'description' => 'Previous Charges',
2032 'subtotal' => $other_money_char.
2033 sprintf('%.2f', $pr_total),
2037 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2038 'subtotal' => $taxtotal }; # adjusted below
2040 my $adjusttotal = 0;
2041 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2042 'subtotal' => 0 }; # adjusted below
2044 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2045 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2046 my $late_sections = [];
2047 if ( $multisection ) {
2048 push @sections, $self->_items_sections( $late_sections );
2050 push @sections, { 'description' => '', 'subtotal' => '' };
2053 foreach my $line_item ( $conf->exists('disable_previous_balance')
2055 : $self->_items_previous
2059 ext_description => [],
2061 $detail->{'ref'} = $line_item->{'pkgnum'};
2062 $detail->{'quantity'} = 1;
2063 $detail->{'section'} = $previous_section;
2064 $detail->{'description'} = &$escape_function($line_item->{'description'});
2065 if ( exists $line_item->{'ext_description'} ) {
2066 @{$detail->{'ext_description'}} = map {
2067 &$escape_function($_);
2068 } @{$line_item->{'ext_description'}};
2070 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2071 $line_item->{'amount'};
2072 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2074 push @detail_items, $detail;
2075 push @buf, [ $detail->{'description'},
2076 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2080 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2081 push @buf, ['','-----------'];
2082 push @buf, [ 'Total Previous Balance',
2083 $money_char. sprintf("%10.2f", $pr_total) ];
2087 foreach my $section (@sections, @$late_sections) {
2089 $section->{'subtotal'} = $other_money_char.
2090 sprintf('%.2f', $section->{'subtotal'})
2093 if ( $section->{'description'} ) {
2094 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2100 $options{'section'} = $section if $multisection;
2101 $options{'format'} = $format;
2102 $options{'escape_function'} = $escape_function;
2103 $options{'format_function'} = sub { () } unless $unsquelched;
2104 $options{'unsquelched'} = $unsquelched;
2106 foreach my $line_item ( $self->_items_pkg(%options) ) {
2108 ext_description => [],
2110 $detail->{'ref'} = $line_item->{'pkgnum'};
2111 $detail->{'quantity'} = $line_item->{'quantity'};
2112 $detail->{'section'} = $section;
2113 $detail->{'description'} = &$escape_function($line_item->{'description'});
2114 if ( exists $line_item->{'ext_description'} ) {
2115 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2117 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2118 $line_item->{'amount'};
2119 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2120 $line_item->{'unit_amount'};
2121 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2123 push @detail_items, $detail;
2124 push @buf, ( [ $detail->{'description'},
2125 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2127 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2131 if ( $section->{'description'} ) {
2132 push @buf, ( ['','-----------'],
2133 [ $section->{'description'}. ' sub-total',
2134 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2143 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2144 unshift @sections, $previous_section if $pr_total;
2147 foreach my $tax ( $self->_items_tax ) {
2149 $total->{'total_item'} = &$escape_function($tax->{'description'});
2150 $taxtotal += $tax->{'amount'};
2151 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2152 if ( $multisection ) {
2153 my $money = $old_latex ? '' : $money_char;
2154 push @detail_items, {
2155 ext_description => [],
2158 description => &$escape_function($tax->{'description'}),
2159 amount => $money. $tax->{'amount'},
2161 section => $tax_section,
2164 push @total_items, $total;
2166 push @buf,[ $total->{'total_item'},
2167 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2174 $total->{'total_item'} = 'Sub-total';
2175 $total->{'total_amount'} =
2176 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2178 if ( $multisection ) {
2179 $tax_section->{'subtotal'} = $other_money_char.
2180 sprintf('%.2f', $taxtotal);
2181 $tax_section->{'pretotal'} = 'New charges sub-total '.
2182 $total->{'total_amount'};
2183 push @sections, $tax_section if $taxtotal;
2185 unshift @total_items, $total;
2188 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2190 push @buf,['','-----------'];
2191 push @buf,[( $conf->exists('disable_previous_balance')
2193 : 'Total New Charges'
2195 $money_char. sprintf("%10.2f",$self->charged) ];
2200 $total->{'total_item'} = &$embolden_function('Total');
2201 $total->{'total_amount'} =
2202 &$embolden_function(
2205 $self->charged + ( $conf->exists('disable_previous_balance')
2211 if ( $multisection ) {
2212 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2213 sprintf('%.2f', $self->charged );
2215 push @total_items, $total;
2217 push @buf,['','-----------'];
2218 push @buf,['Total Charges',
2220 sprintf( '%10.2f', $self->charged +
2221 ( $conf->exists('disable_previous_balance')
2230 unless ( $conf->exists('disable_previous_balance') ) {
2231 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2234 my $credittotal = 0;
2235 foreach my $credit ( $self->_items_credits ) {
2237 $total->{'total_item'} = &$escape_function($credit->{'description'});
2238 $credittotal += $credit->{'amount'};
2239 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2240 $adjusttotal += $credit->{'amount'};
2241 if ( $multisection ) {
2242 my $money = $old_latex ? '' : $money_char;
2243 push @detail_items, {
2244 ext_description => [],
2247 description => &$escape_function($credit->{'description'}),
2248 amount => $money. $credit->{'amount'},
2250 section => $adjust_section,
2253 push @total_items, $total;
2256 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2259 foreach ( $self->cust_credited ) {
2261 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2263 my $reason = substr($_->cust_credit->reason,0,32);
2264 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2265 $reason = " ($reason) " if $reason;
2267 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2268 $money_char. sprintf("%10.2f",$_->amount)
2273 my $paymenttotal = 0;
2274 foreach my $payment ( $self->_items_payments ) {
2276 $total->{'total_item'} = &$escape_function($payment->{'description'});
2277 $paymenttotal += $payment->{'amount'};
2278 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2279 $adjusttotal += $payment->{'amount'};
2280 if ( $multisection ) {
2281 my $money = $old_latex ? '' : $money_char;
2282 push @detail_items, {
2283 ext_description => [],
2286 description => &$escape_function($payment->{'description'}),
2287 amount => $money. $payment->{'amount'},
2289 section => $adjust_section,
2292 push @total_items, $total;
2294 push @buf, [ $payment->{'description'},
2295 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2298 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2300 if ( $multisection ) {
2301 $adjust_section->{'subtotal'} = $other_money_char.
2302 sprintf('%.2f', $adjusttotal);
2303 push @sections, $adjust_section;
2308 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2309 $total->{'total_amount'} =
2310 &$embolden_function(
2311 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2313 if ( $multisection ) {
2314 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2315 $total->{'total_amount'};
2317 push @total_items, $total;
2319 push @buf,['','-----------'];
2320 push @buf,[$self->balance_due_msg, $money_char.
2321 sprintf("%10.2f", $balance_due ) ];
2325 if ( $multisection ) {
2326 push @sections, @$late_sections
2332 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2333 /invoice_lines\((\d*)\)/;
2334 $invoice_lines += $1 || scalar(@buf);
2337 die "no invoice_lines() functions in template?"
2338 if ( $format eq 'template' && !$wasfunc );
2340 if ($format eq 'template') {
2342 if ( $invoice_lines ) {
2343 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2344 $invoice_data{'total_pages'}++
2345 if scalar(@buf) % $invoice_lines;
2348 #setup subroutine for the template
2349 sub FS::cust_bill::_template::invoice_lines {
2350 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2352 scalar(@FS::cust_bill::_template::buf)
2353 ? shift @FS::cust_bill::_template::buf
2362 push @collect, split("\n",
2363 $text_template->fill_in( HASH => \%invoice_data,
2364 PACKAGE => 'FS::cust_bill::_template'
2367 $FS::cust_bill::_template::page++;
2369 map "$_\n", @collect;
2371 warn "filling in template for invoice ". $self->invnum. "\n"
2373 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2376 $text_template->fill_in(HASH => \%invoice_data);
2380 =item print_ps [ TIME [ , TEMPLATE ] ]
2382 Returns an postscript 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.
2394 my ($file, $lfile) = $self->print_latex(@_);
2395 my $ps = generate_ps($file);
2401 =item print_pdf [ TIME [ , TEMPLATE ] ]
2403 Returns an PDF invoice, as a scalar.
2405 TIME an optional value used to control the printing of overdue messages. The
2406 default is now. It isn't the date of the invoice; that's the `_date' field.
2407 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2408 L<Time::Local> and L<Date::Parse> for conversion functions.
2415 my ($file, $lfile) = $self->print_latex(@_);
2416 my $pdf = generate_pdf($file);
2422 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2424 Returns an HTML invoice, as a scalar.
2426 TIME an optional value used to control the printing of overdue messages. The
2427 default is now. It isn't the date of the invoice; that's the `_date' field.
2428 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2429 L<Time::Local> and L<Date::Parse> for conversion functions.
2431 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2432 when emailing the invoice as part of a multipart/related MIME email.
2440 %params = %{ shift() };
2442 $params{'time'} = shift;
2443 $params{'template'} = shift;
2444 $params{'cid'} = shift;
2447 $params{'format'} = 'html';
2449 $self->print_generic( %params );
2452 # quick subroutine for print_latex
2454 # There are ten characters that LaTeX treats as special characters, which
2455 # means that they do not simply typeset themselves:
2456 # # $ % & ~ _ ^ \ { }
2458 # TeX ignores blanks following an escaped character; if you want a blank (as
2459 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2463 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2464 $value =~ s/([<>])/\$$1\$/g;
2468 #utility methods for print_*
2470 sub _translate_old_latex_format {
2471 warn "_translate_old_latex_format called\n"
2478 if ( $line =~ /^%%Detail\s*$/ ) {
2480 push @template, q![@--!,
2481 q! foreach my $_tr_line (@detail_items) {!,
2482 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2483 q! $_tr_line->{'description'} .= !,
2484 q! "\\tabularnewline\n~~".!,
2485 q! join( "\\tabularnewline\n~~",!,
2486 q! @{$_tr_line->{'ext_description'}}!,
2490 while ( ( my $line_item_line = shift )
2491 !~ /^%%EndDetail\s*$/ ) {
2492 $line_item_line =~ s/'/\\'/g; # nice LTS
2493 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2494 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2495 push @template, " \$OUT .= '$line_item_line';";
2498 push @template, '}',
2501 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2503 push @template, '[@--',
2504 ' foreach my $_tr_line (@total_items) {';
2506 while ( ( my $total_item_line = shift )
2507 !~ /^%%EndTotalDetails\s*$/ ) {
2508 $total_item_line =~ s/'/\\'/g; # nice LTS
2509 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2510 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2511 push @template, " \$OUT .= '$total_item_line';";
2514 push @template, '}',
2518 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2519 push @template, $line;
2525 warn "$_\n" foreach @template;
2534 #check for an invoice- specific override (eventually)
2536 #check for a customer- specific override
2537 return $self->cust_main->invoice_terms
2538 if $self->cust_main->invoice_terms;
2540 #use configured default or default default
2541 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2547 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2548 $duedate = $self->_date() + ( $1 * 86400 );
2555 $self->due_date ? time2str(shift, $self->due_date) : '';
2558 sub balance_due_msg {
2560 my $msg = 'Balance Due';
2561 return $msg unless $self->terms;
2562 if ( $self->due_date ) {
2563 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2564 } elsif ( $self->terms ) {
2565 $msg .= ' - '. $self->terms;
2570 sub balance_due_date {
2573 if ( $conf->exists('invoice_default_terms')
2574 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2575 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2580 =item invnum_date_pretty
2582 Returns a string with the invoice number and date, for example:
2583 "Invoice #54 (3/20/2008)"
2587 sub invnum_date_pretty {
2589 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2592 sub _items_sections {
2599 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2602 if ( $cust_bill_pkg->pkgnum > 0 ) {
2603 my $usage = $cust_bill_pkg->usage;
2605 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2606 my $desc = $display->section;
2607 my $type = $display->type;
2609 if ( $display->post_total ) {
2610 if (! $type || $type eq 'S') {
2611 $l{$desc} += $cust_bill_pkg->setup
2612 if ( $cust_bill_pkg->setup != 0 );
2616 $l{$desc} += $cust_bill_pkg->recur
2617 if ( $cust_bill_pkg->recur != 0 );
2620 if ($type && $type eq 'R') {
2621 $l{$desc} += $cust_bill_pkg->recur - $usage
2622 if ( $cust_bill_pkg->recur != 0 );
2625 if ($type && $type eq 'U') {
2626 $l{$desc} += $usage;
2630 if (! $type || $type eq 'S') {
2631 $s{$desc} += $cust_bill_pkg->setup
2632 if ( $cust_bill_pkg->setup != 0 );
2636 $s{$desc} += $cust_bill_pkg->recur
2637 if ( $cust_bill_pkg->recur != 0 );
2640 if ($type && $type eq 'R') {
2641 $s{$desc} += $cust_bill_pkg->recur - $usage
2642 if ( $cust_bill_pkg->recur != 0 );
2645 if ($type && $type eq 'U') {
2646 $s{$desc} += $usage;
2657 push @$late, map { { 'description' => $_,
2658 'subtotal' => $l{$_},
2662 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2669 #my @display = scalar(@_)
2671 # : qw( _items_previous _items_pkg );
2672 # #: qw( _items_pkg );
2673 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2674 my @display = qw( _items_previous _items_pkg );
2677 foreach my $display ( @display ) {
2678 push @b, $self->$display(@_);
2683 sub _items_previous {
2685 my $cust_main = $self->cust_main;
2686 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2688 foreach ( @pr_cust_bill ) {
2690 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2691 ' ('. time2str('%x',$_->_date). ')',
2692 #'pkgpart' => 'N/A',
2694 'amount' => sprintf("%.2f", $_->owed),
2700 # 'description' => 'Previous Balance',
2701 # #'pkgpart' => 'N/A',
2702 # 'pkgnum' => 'N/A',
2703 # 'amount' => sprintf("%10.2f", $pr_total ),
2704 # 'ext_description' => [ map {
2705 # "Invoice ". $_->invnum.
2706 # " (". time2str("%x",$_->_date). ") ".
2707 # sprintf("%10.2f", $_->owed)
2708 # } @pr_cust_bill ],
2715 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2716 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2720 return 0 unless $a cmp $b;
2721 return -1 if $b eq 'Tax';
2722 return 1 if $a eq 'Tax';
2723 return -1 if $b eq 'Other surcharges';
2724 return 1 if $a eq 'Other surcharges';
2730 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2731 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2734 sub _items_cust_bill_pkg {
2736 my $cust_bill_pkg = shift;
2739 my $format = $opt{format} || '';
2740 my $escape_function = $opt{escape_function} || sub { shift };
2741 my $format_function = $opt{format_function} || '';
2742 my $unsquelched = $opt{unsquelched} || '';
2743 my $section = $opt{section}->{description} if $opt{section};
2746 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2748 foreach my $display ( grep { defined($section)
2749 ? $_->section eq $section
2752 $cust_bill_pkg->cust_bill_pkg_display
2756 my $type = $display->type;
2758 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2760 my $desc = $cust_bill_pkg->desc;
2761 $desc = substr($desc, 0, 50). '...'
2762 if $format eq 'latex' && length($desc) > 50;
2764 my %details_opt = ( 'format' => $format,
2765 'escape_function' => $escape_function,
2766 'format_function' => $format_function,
2769 if ( $cust_bill_pkg->pkgnum > 0 ) {
2771 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2773 my $description = $desc;
2774 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2776 my @d = map &{$escape_function}($_),
2777 $cust_pkg->h_labels_short($self->_date);
2778 push @d, $cust_bill_pkg->details(%details_opt)
2779 if $cust_bill_pkg->recur == 0;
2782 description => $description,
2783 #pkgpart => $part_pkg->pkgpart,
2784 pkgnum => $cust_bill_pkg->pkgnum,
2785 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2786 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2787 quantity => $cust_bill_pkg->quantity,
2788 ext_description => \@d,
2793 if ( $cust_bill_pkg->recur != 0 &&
2794 ( !$type || $type eq 'R' || $type eq 'U' )
2798 my $is_summary = $display->summary;
2799 my $description = $is_summary ? "Usage charges" : $desc;
2801 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2802 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2803 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2806 #at least until cust_bill_pkg has "past" ranges in addition to
2807 #the "future" sdate/edate ones... see #3032
2809 push @d, map &{$escape_function}($_),
2810 $cust_pkg->h_labels_short($self->_date)
2811 #$cust_bill_pkg->edate,
2812 #$cust_bill_pkg->sdate),
2815 @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2816 push @d, $cust_bill_pkg->details(%details_opt)
2817 unless ($is_summary || $type && $type eq 'R');
2821 $amount = $cust_bill_pkg->recur;
2822 }elsif($type eq 'R') {
2823 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2824 }elsif($type eq 'U') {
2825 $amount = $cust_bill_pkg->usage;
2829 description => $description,
2830 #pkgpart => $part_pkg->pkgpart,
2831 pkgnum => $cust_bill_pkg->pkgnum,
2832 amount => sprintf("%.2f", $amount),
2833 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2834 quantity => $cust_bill_pkg->quantity,
2835 ext_description => \@d,
2836 } unless ( $type eq 'U' && ! $amount );
2840 } else { #pkgnum tax or one-shot line item (??)
2842 if ( $cust_bill_pkg->setup != 0 ) {
2844 'description' => $desc,
2845 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2848 if ( $cust_bill_pkg->recur != 0 ) {
2850 'description' => "$desc (".
2851 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2852 time2str("%x", $cust_bill_pkg->edate). ')',
2853 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2867 sub _items_credits {
2872 foreach ( $self->cust_credited ) {
2874 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2876 my $reason = $_->cust_credit->reason;
2877 #my $reason = substr($_->cust_credit->reason,0,32);
2878 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2879 $reason = " ($reason) " if $reason;
2881 #'description' => 'Credit ref\#'. $_->crednum.
2882 # " (". time2str("%x",$_->cust_credit->_date) .")".
2884 'description' => 'Credit applied '.
2885 time2str("%x",$_->cust_credit->_date). $reason,
2886 'amount' => sprintf("%.2f",$_->amount),
2889 #foreach ( @cr_cust_credit ) {
2891 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2892 # $money_char. sprintf("%10.2f",$_->credited)
2900 sub _items_payments {
2904 #get & print payments
2905 foreach ( $self->cust_bill_pay ) {
2907 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2910 'description' => "Payment received ".
2911 time2str("%x",$_->cust_pay->_date ),
2912 'amount' => sprintf("%.2f", $_->amount )
2927 =item process_reprint
2931 sub process_reprint {
2932 process_re_X('print', @_);
2935 =item process_reemail
2939 sub process_reemail {
2940 process_re_X('email', @_);
2948 process_re_X('fax', @_);
2956 process_re_X('ftp', @_);
2963 sub process_respool {
2964 process_re_X('spool', @_);
2967 use Storable qw(thaw);
2971 my( $method, $job ) = ( shift, shift );
2972 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2974 my $param = thaw(decode_base64(shift));
2975 warn Dumper($param) if $DEBUG;
2986 my($method, $job, %param ) = @_;
2988 warn "re_X $method for job $job with param:\n".
2989 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2992 #some false laziness w/search/cust_bill.html
2994 my $orderby = 'ORDER BY cust_bill._date';
2996 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2998 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3000 my @cust_bill = qsearch( {
3001 #'select' => "cust_bill.*",
3002 'table' => 'cust_bill',
3003 'addl_from' => $addl_from,
3005 'extra_sql' => $extra_sql,
3006 'order_by' => $orderby,
3010 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3012 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3015 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3016 foreach my $cust_bill ( @cust_bill ) {
3017 $cust_bill->$method();
3019 if ( $job ) { #progressbar foo
3021 if ( time - $min_sec > $last ) {
3022 my $error = $job->update_statustext(
3023 int( 100 * $num / scalar(@cust_bill) )
3025 die $error if $error;
3036 =head1 CLASS METHODS
3042 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3048 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3053 Returns an SQL fragment to retreive the net amount (charged minus credited).
3059 'charged - '. $class->credited_sql;
3064 Returns an SQL fragment to retreive the amount paid against this invoice.
3070 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3071 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3076 Returns an SQL fragment to retreive the amount credited against this invoice.
3082 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3083 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3086 =item search_sql HASHREF
3088 Class method which returns an SQL WHERE fragment to search for parameters
3089 specified in HASHREF. Valid parameters are
3095 Epoch date (UNIX timestamp) setting a lower bound for _date values
3099 Epoch date (UNIX timestamp) setting an upper bound for _date values
3113 =item newest_percust
3117 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3122 my($class, $param) = @_;
3124 warn "$me search_sql called with params: \n".
3125 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3130 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3131 push @search, "cust_bill._date >= $1";
3133 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3134 push @search, "cust_bill._date < $1";
3136 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3137 push @search, "cust_bill.invnum >= $1";
3139 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3140 push @search, "cust_bill.invnum <= $1";
3142 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3143 push @search, "cust_main.agentnum = $1";
3146 push @search, '0 != '. FS::cust_bill->owed_sql
3147 if $param->{'open'};
3149 push @search, '0 != '. FS::cust_bill->net_sql
3152 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3153 if $param->{'days'};
3155 if ( $param->{'newest_percust'} ) {
3157 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3158 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3160 my @newest_where = map { my $x = $_;
3161 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3164 grep ! /^cust_main./, @search;
3165 my $newest_where = scalar(@newest_where)
3166 ? ' AND '. join(' AND ', @newest_where)
3170 push @search, "cust_bill._date = (
3171 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3172 WHERE newest_cust_bill.custnum = cust_bill.custnum
3178 my $curuser = $FS::CurrentUser::CurrentUser;
3179 if ( $curuser->username eq 'fs_queue'
3180 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3182 my $newuser = qsearchs('access_user', {
3183 'username' => $username,
3187 $curuser = $newuser;
3189 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3193 push @search, $curuser->agentnums_sql;
3195 join(' AND ', @search );
3207 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3208 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base