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 || #XXX should go away
847 $conf->config('invoice_from', $self->cust_main->agentnum )
850 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
853 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
855 my @invoicing_list = $self->cust_main->invoicing_list;
857 #$self->email_invoice($template, $invoice_from)
858 $self->email($template, $invoice_from)
859 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
861 #$self->print_invoice($template)
862 $self->print($template)
863 if grep { $_ eq 'POST' } @invoicing_list; #postal
865 $self->fax_invoice($template)
866 if grep { $_ eq 'FAX' } @invoicing_list; #fax
872 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
876 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
878 INVOICE_FROM, if specified, overrides the default email invoice From: address.
882 sub queueable_email {
885 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
886 or die "invalid invoice number: " . $opt{invnum};
888 my @args = ( $opt{template} );
889 push @args, $opt{invoice_from}
890 if exists($opt{invoice_from}) && $opt{invoice_from};
892 my $error = $self->email( @args );
893 die $error if $error;
900 my $template = scalar(@_) ? shift : '';
904 : ( $self->_agent_invoice_from || #XXX should go away
905 $conf->config('invoice_from', $self->cust_main->agentnum )
909 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
910 $self->cust_main->invoicing_list;
912 #better to notify this person than silence
913 @invoicing_list = ($invoice_from) unless @invoicing_list;
915 my $subject = $self->email_subject($template);
917 my $error = send_email(
918 $self->generate_email(
919 'from' => $invoice_from,
920 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
921 'subject' => $subject,
922 'template' => $template,
925 die "can't email invoice: $error\n" if $error;
926 #die "$error\n" if $error;
933 #my $template = scalar(@_) ? shift : '';
936 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
939 my $cust_main = $self->cust_main;
940 my $name = $cust_main->name;
941 my $name_short = $cust_main->name_short;
942 my $invoice_number = $self->invnum;
943 my $invoice_date = $self->_date_pretty;
948 =item lpr_data [ TEMPLATENAME ]
950 Returns the postscript or plaintext for this invoice as an arrayref.
952 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
957 my( $self, $template) = @_;
958 $conf->exists('invoice_latex')
959 ? [ $self->print_ps('', $template) ]
960 : [ $self->print_text('', $template) ];
963 =item print [ TEMPLATENAME ]
967 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
974 my $template = scalar(@_) ? shift : '';
976 do_print $self->lpr_data($template);
979 =item fax_invoice [ TEMPLATENAME ]
983 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
989 my $template = scalar(@_) ? shift : '';
991 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
992 unless $conf->exists('invoice_latex');
994 my $dialstring = $self->cust_main->getfield('fax');
997 my $error = send_fax( 'docdata' => $self->lpr_data($template),
998 'dialstring' => $dialstring,
1000 die $error if $error;
1004 =item ftp_invoice [ TEMPLATENAME ]
1006 Sends this invoice data via FTP.
1008 TEMPLATENAME is unused?
1014 my $template = scalar(@_) ? shift : '';
1017 'protocol' => 'ftp',
1018 'server' => $conf->config('cust_bill-ftpserver'),
1019 'username' => $conf->config('cust_bill-ftpusername'),
1020 'password' => $conf->config('cust_bill-ftppassword'),
1021 'dir' => $conf->config('cust_bill-ftpdir'),
1022 'format' => $conf->config('cust_bill-ftpformat'),
1026 =item spool_invoice [ TEMPLATENAME ]
1028 Spools this invoice data (see L<FS::spool_csv>)
1030 TEMPLATENAME is unused?
1036 my $template = scalar(@_) ? shift : '';
1039 'format' => $conf->config('cust_bill-spoolformat'),
1040 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1044 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1046 Like B<send>, but only sends the invoice if it is the newest open invoice for
1051 sub send_if_newest {
1056 grep { $_->owed > 0 }
1057 qsearch('cust_bill', {
1058 'custnum' => $self->custnum,
1059 #'_date' => { op=>'>', value=>$self->_date },
1060 'invnum' => { op=>'>', value=>$self->invnum },
1067 =item send_csv OPTION => VALUE, ...
1069 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1073 protocol - currently only "ftp"
1079 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1080 and YYMMDDHHMMSS is a timestamp.
1082 See L</print_csv> for a description of the output format.
1087 my($self, %opt) = @_;
1091 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1092 mkdir $spooldir, 0700 unless -d $spooldir;
1094 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1095 my $file = "$spooldir/$tracctnum.csv";
1097 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1099 open(CSV, ">$file") or die "can't open $file: $!";
1107 if ( $opt{protocol} eq 'ftp' ) {
1108 eval "use Net::FTP;";
1110 $net = Net::FTP->new($opt{server}) or die @$;
1112 die "unknown protocol: $opt{protocol}";
1115 $net->login( $opt{username}, $opt{password} )
1116 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1118 $net->binary or die "can't set binary mode";
1120 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1122 $net->put($file) or die "can't put $file: $!";
1132 Spools CSV invoice data.
1138 =item format - 'default' or 'billco'
1140 =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>).
1142 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1144 =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.
1151 my($self, %opt) = @_;
1153 my $cust_main = $self->cust_main;
1155 if ( $opt{'dest'} ) {
1156 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1157 $cust_main->invoicing_list;
1158 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1159 || ! keys %invoicing_list;
1162 if ( $opt{'balanceover'} ) {
1164 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1167 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1168 mkdir $spooldir, 0700 unless -d $spooldir;
1170 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1174 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1175 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1178 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1180 open(CSV, ">>$file") or die "can't open $file: $!";
1181 flock(CSV, LOCK_EX);
1186 if ( lc($opt{'format'}) eq 'billco' ) {
1188 flock(CSV, LOCK_UN);
1193 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1196 open(CSV,">>$file") or die "can't open $file: $!";
1197 flock(CSV, LOCK_EX);
1203 flock(CSV, LOCK_UN);
1210 =item print_csv OPTION => VALUE, ...
1212 Returns CSV data for this invoice.
1216 format - 'default' or 'billco'
1218 Returns a list consisting of two scalars. The first is a single line of CSV
1219 header information for this invoice. The second is one or more lines of CSV
1220 detail information for this invoice.
1222 If I<format> is not specified or "default", the fields of the CSV file are as
1225 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1229 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1231 B<record_type> is C<cust_bill> for the initial header line only. The
1232 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1233 fields are filled in.
1235 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1236 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1239 =item invnum - invoice number
1241 =item custnum - customer number
1243 =item _date - invoice date
1245 =item charged - total invoice amount
1247 =item first - customer first name
1249 =item last - customer first name
1251 =item company - company name
1253 =item address1 - address line 1
1255 =item address2 - address line 1
1265 =item pkg - line item description
1267 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1269 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1271 =item sdate - start date for recurring fee
1273 =item edate - end date for recurring fee
1277 If I<format> is "billco", the fields of the header CSV file are as follows:
1279 +-------------------------------------------------------------------+
1280 | FORMAT HEADER FILE |
1281 |-------------------------------------------------------------------|
1282 | Field | Description | Name | Type | Width |
1283 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1284 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1285 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1286 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1287 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1288 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1289 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1290 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1291 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1292 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1293 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1294 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1295 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1296 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1297 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1298 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1299 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1300 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1301 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1302 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1303 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1304 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1305 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1306 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1307 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1308 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1309 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1310 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1311 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1312 +-------+-------------------------------+------------+------+-------+
1314 If I<format> is "billco", the fields of the detail CSV file are as follows:
1316 FORMAT FOR DETAIL FILE
1318 Field | Description | Name | Type | Width
1319 1 | N/A-Leave Empty | RC | CHAR | 2
1320 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1321 3 | Account Number | TRACCTNUM | CHAR | 15
1322 4 | Invoice Number | TRINVOICE | CHAR | 15
1323 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1324 6 | Transaction Detail | DETAILS | CHAR | 100
1325 7 | Amount | AMT | NUM* | 9
1326 8 | Line Format Control** | LNCTRL | CHAR | 2
1327 9 | Grouping Code | GROUP | CHAR | 2
1328 10 | User Defined | ACCT CODE | CHAR | 15
1333 my($self, %opt) = @_;
1335 eval "use Text::CSV_XS";
1338 my $cust_main = $self->cust_main;
1340 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1342 if ( lc($opt{'format'}) eq 'billco' ) {
1345 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1347 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1349 my( $previous_balance, @unused ) = $self->previous; #previous balance
1351 my $pmt_cr_applied = 0;
1352 $pmt_cr_applied += $_->{'amount'}
1353 foreach ( $self->_items_payments, $self->_items_credits ) ;
1355 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1358 '', # 1 | N/A-Leave Empty CHAR 2
1359 '', # 2 | N/A-Leave Empty CHAR 15
1360 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1361 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1362 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1363 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1364 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1365 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1366 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1367 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1368 '', # 10 | Ancillary Billing Information CHAR 30
1369 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1370 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1373 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1376 $duedate, # 14 | Bill Due Date CHAR 10
1378 $previous_balance, # 15 | Previous Balance NUM* 9
1379 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1380 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1381 $totaldue, # 18 | Total Amt Due NUM* 9
1382 $totaldue, # 19 | Total Amt Due NUM* 9
1383 '', # 20 | 30 Day Aging NUM* 9
1384 '', # 21 | 60 Day Aging NUM* 9
1385 '', # 22 | 90 Day Aging NUM* 9
1386 'N', # 23 | Y/N CHAR 1
1387 '', # 24 | Remittance automation CHAR 100
1388 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1389 $self->custnum, # 26 | Customer Reference Number CHAR 15
1390 '0', # 27 | Federal Tax*** NUM* 9
1391 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1392 '0', # 29 | Other Taxes & Fees*** NUM* 9
1401 time2str("%x", $self->_date),
1402 sprintf("%.2f", $self->charged),
1403 ( map { $cust_main->getfield($_) }
1404 qw( first last company address1 address2 city state zip country ) ),
1406 ) or die "can't create csv";
1409 my $header = $csv->string. "\n";
1412 if ( lc($opt{'format'}) eq 'billco' ) {
1415 foreach my $item ( $self->_items_pkg ) {
1418 '', # 1 | N/A-Leave Empty CHAR 2
1419 '', # 2 | N/A-Leave Empty CHAR 15
1420 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1421 $self->invnum, # 4 | Invoice Number CHAR 15
1422 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1423 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1424 $item->{'amount'}, # 7 | Amount NUM* 9
1425 '', # 8 | Line Format Control** CHAR 2
1426 '', # 9 | Grouping Code CHAR 2
1427 '', # 10 | User Defined CHAR 15
1430 $detail .= $csv->string. "\n";
1436 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1438 my($pkg, $setup, $recur, $sdate, $edate);
1439 if ( $cust_bill_pkg->pkgnum ) {
1441 ($pkg, $setup, $recur, $sdate, $edate) = (
1442 $cust_bill_pkg->part_pkg->pkg,
1443 ( $cust_bill_pkg->setup != 0
1444 ? sprintf("%.2f", $cust_bill_pkg->setup )
1446 ( $cust_bill_pkg->recur != 0
1447 ? sprintf("%.2f", $cust_bill_pkg->recur )
1449 ( $cust_bill_pkg->sdate
1450 ? time2str("%x", $cust_bill_pkg->sdate)
1452 ($cust_bill_pkg->edate
1453 ?time2str("%x", $cust_bill_pkg->edate)
1457 } else { #pkgnum tax
1458 next unless $cust_bill_pkg->setup != 0;
1459 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1460 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1462 ($pkg, $setup, $recur, $sdate, $edate) =
1463 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1469 ( map { '' } (1..11) ),
1470 ($pkg, $setup, $recur, $sdate, $edate)
1471 ) or die "can't create csv";
1473 $detail .= $csv->string. "\n";
1479 ( $header, $detail );
1485 Pays this invoice with a compliemntary payment. If there is an error,
1486 returns the error, otherwise returns false.
1492 my $cust_pay = new FS::cust_pay ( {
1493 'invnum' => $self->invnum,
1494 'paid' => $self->owed,
1497 'payinfo' => $self->cust_main->payinfo,
1505 Attempts to pay this invoice with a credit card payment via a
1506 Business::OnlinePayment realtime gateway. See
1507 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1508 for supported processors.
1514 $self->realtime_bop( 'CC', @_ );
1519 Attempts to pay this invoice with an electronic check (ACH) payment via a
1520 Business::OnlinePayment realtime gateway. See
1521 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1522 for supported processors.
1528 $self->realtime_bop( 'ECHECK', @_ );
1533 Attempts to pay this invoice with phone bill (LEC) payment via a
1534 Business::OnlinePayment realtime gateway. See
1535 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1536 for supported processors.
1542 $self->realtime_bop( 'LEC', @_ );
1546 my( $self, $method ) = @_;
1548 my $cust_main = $self->cust_main;
1549 my $balance = $cust_main->balance;
1550 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1551 $amount = sprintf("%.2f", $amount);
1552 return "not run (balance $balance)" unless $amount > 0;
1554 my $description = 'Internet Services';
1555 if ( $conf->exists('business-onlinepayment-description') ) {
1556 my $dtempl = $conf->config('business-onlinepayment-description');
1558 my $agent_obj = $cust_main->agent
1559 or die "can't retreive agent for $cust_main (agentnum ".
1560 $cust_main->agentnum. ")";
1561 my $agent = $agent_obj->agent;
1562 my $pkgs = join(', ',
1563 map { $_->part_pkg->pkg }
1564 grep { $_->pkgnum } $self->cust_bill_pkg
1566 $description = eval qq("$dtempl");
1569 $cust_main->realtime_bop($method, $amount,
1570 'description' => $description,
1571 'invnum' => $self->invnum,
1576 =item batch_card OPTION => VALUE...
1578 Adds a payment for this invoice to the pending credit card batch (see
1579 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1580 runs the payment using a realtime gateway.
1585 my ($self, %options) = @_;
1586 my $cust_main = $self->cust_main;
1588 $options{invnum} = $self->invnum;
1590 $cust_main->batch_card(%options);
1593 sub _agent_template {
1595 $self->cust_main->agent_template;
1598 sub _agent_invoice_from {
1600 $self->cust_main->agent_invoice_from;
1603 =item print_text [ TIME [ , TEMPLATE ] ]
1605 Returns an text invoice, as a list of lines.
1607 TIME an optional value used to control the printing of overdue messages. The
1608 default is now. It isn't the date of the invoice; that's the `_date' field.
1609 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1610 L<Time::Local> and L<Date::Parse> for conversion functions.
1615 my( $self, $today, $template ) = @_;
1617 my %params = ( 'format' => 'template' );
1618 $params{'time'} = $today if $today;
1619 $params{'template'} = $template if $template;
1621 $self->print_generic( %params );
1624 =item print_latex [ TIME [ , TEMPLATE ] ]
1626 Internal method - returns a filename of a filled-in LaTeX template for this
1627 invoice (Note: add ".tex" to get the actual filename), and a filename of
1628 an associated logo (with the .eps extension included).
1630 See print_ps and print_pdf for methods that return PostScript and PDF output.
1632 TIME an optional value used to control the printing of overdue messages. The
1633 default is now. It isn't the date of the invoice; that's the `_date' field.
1634 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1635 L<Time::Local> and L<Date::Parse> for conversion functions.
1640 my( $self, $today, $template ) = @_;
1642 my %params = ( 'format' => 'latex' );
1643 $params{'time'} = $today if $today;
1644 $params{'template'} = $template if $template;
1646 $template ||= $self->_agent_template;
1648 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1649 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1653 ) or die "can't open temp file: $!\n";
1655 my $agentnum = $self->cust_main->agentnum;
1657 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1658 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1659 or die "can't write temp file: $!\n";
1661 print $lh $conf->config_binary('logo.eps', $agentnum)
1662 or die "can't write temp file: $!\n";
1665 $params{'logo_file'} = $lh->filename;
1667 my @filled_in = $self->print_generic( %params );
1669 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1673 ) or die "can't open temp file: $!\n";
1674 print $fh join('', @filled_in );
1677 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1678 return ($1, $params{'logo_file'});
1682 =item print_generic OPTIONS_HASH
1684 Internal method - returns a filled-in template for this invoice as a scalar.
1686 See print_ps and print_pdf for methods that return PostScript and PDF output.
1688 Non optional options include
1689 format - latex, html, template
1691 Optional options include
1693 template - a value used as a suffix for a configuration template
1695 time - a value used to control the printing of overdue messages. The
1696 default is now. It isn't the date of the invoice; that's the `_date' field.
1697 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1698 L<Time::Local> and L<Date::Parse> for conversion functions.
1702 unsquelch_cdr - overrides any per customer cdr squelching when true
1706 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1707 # (alignment?) problems to change them all to '%.2f' ?
1710 my( $self, %params ) = @_;
1711 my $today = $params{today} ? $params{today} : time;
1712 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1715 my $format = $params{format};
1716 die "Unknown format: $format"
1717 unless $format =~ /^(latex|html|template)$/;
1719 my $cust_main = $self->cust_main;
1720 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1721 unless $cust_main->payname
1722 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1724 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1725 'html' => [ '<%=', '%>' ],
1726 'template' => [ '{', '}' ],
1729 #create the template
1730 my $template = $params{template} ? $params{template} : $self->_agent_template;
1731 my $templatefile = "invoice_$format";
1732 $templatefile .= "_$template"
1733 if length($template);
1734 my @invoice_template = map "$_\n", $conf->config($templatefile)
1735 or die "cannot load config data $templatefile";
1738 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1739 #change this to a die when the old code is removed
1740 warn "old-style invoice template $templatefile; ".
1741 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1742 $old_latex = 'true';
1743 @invoice_template = _translate_old_latex_format(@invoice_template);
1746 my $text_template = new Text::Template(
1748 SOURCE => \@invoice_template,
1749 DELIMITERS => $delimiters{$format},
1752 $text_template->compile()
1753 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1756 # additional substitution could possibly cause breakage in existing templates
1757 my %convert_maps = (
1759 'notes' => sub { map "$_", @_ },
1760 'footer' => sub { map "$_", @_ },
1761 'smallfooter' => sub { map "$_", @_ },
1762 'returnaddress' => sub { map "$_", @_ },
1763 'coupon' => sub { map "$_", @_ },
1769 s/%%(.*)$/<!-- $1 -->/g;
1770 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1771 s/\\begin\{enumerate\}/<ol>/g;
1773 s/\\end\{enumerate\}/<\/ol>/g;
1774 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1783 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1785 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1790 s/\\\\\*?\s*$/<BR>/;
1791 s/\\hyphenation\{[\w\s\-]+}//;
1796 'coupon' => sub { "" },
1803 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1804 s/\\begin\{enumerate\}//g;
1806 s/\\end\{enumerate\}//g;
1807 s/\\textbf\{(.*)\}/$1/g;
1814 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1816 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1821 s/\\\\\*?\s*$/\n/; # dubious
1822 s/\\hyphenation\{[\w\s\-]+}//;
1826 'coupon' => sub { "" },
1831 # hashes for differing output formats
1832 my %nbsps = ( 'latex' => '~',
1833 'html' => '', # '&nbps;' would be nice
1834 'template' => '', # not used
1836 my $nbsp = $nbsps{$format};
1838 my %escape_functions = ( 'latex' => \&_latex_escape,
1839 'html' => \&encode_entities,
1840 'template' => sub { shift },
1842 my $escape_function = $escape_functions{$format};
1844 my %date_formats = ( 'latex' => '%b %o, %Y',
1845 'html' => '%b %o, %Y',
1848 my $date_format = $date_formats{$format};
1850 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1852 'html' => sub { return '<b>'. shift(). '</b>'
1854 'template' => sub { shift },
1856 my $embolden_function = $embolden_functions{$format};
1859 # generate template variables
1862 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1866 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1872 $returnaddress = join("\n",
1873 $conf->config_orbase("invoice_${format}returnaddress", $template)
1876 } elsif ( grep /\S/,
1877 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1879 my $convert_map = $convert_maps{$format}{'returnaddress'};
1882 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1887 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1889 my $convert_map = $convert_maps{$format}{'returnaddress'};
1890 $returnaddress = join( "\n", &$convert_map(
1891 map { s/( {2,})/'~' x length($1)/eg;
1895 ( $conf->config('company_name', $self->cust_main->agentnum),
1896 $conf->config('company_address', $self->cust_main->agentnum),
1903 my $warning = "Couldn't find a return address; ".
1904 "do you need to set the company_address configuration value?";
1906 $returnaddress = $nbsp;
1907 #$returnaddress = $warning;
1911 my %invoice_data = (
1912 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
1913 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
1914 'custnum' => $cust_main->display_custnum,
1915 'invnum' => $self->invnum,
1916 'date' => time2str($date_format, $self->_date),
1917 'today' => time2str('%b %o, %Y', $today),
1918 'agent' => &$escape_function($cust_main->agent->agent),
1919 'agent_custid' => &$escape_function($cust_main->agent_custid),
1920 'payname' => &$escape_function($cust_main->payname),
1921 'company' => &$escape_function($cust_main->company),
1922 'address1' => &$escape_function($cust_main->address1),
1923 'address2' => &$escape_function($cust_main->address2),
1924 'city' => &$escape_function($cust_main->city),
1925 'state' => &$escape_function($cust_main->state),
1926 'zip' => &$escape_function($cust_main->zip),
1927 'fax' => &$escape_function($cust_main->fax),
1928 'returnaddress' => $returnaddress,
1930 'terms' => $self->terms,
1931 'template' => $template, #params{'template'},
1932 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1933 # better hang on to conf_dir for a while
1934 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1937 'current_charges' => sprintf("%.2f", $self->charged),
1938 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1939 'ship_enable' => $conf->exists('invoice-ship_address'),
1940 'unitprices' => $conf->exists('invoice-unitprice'),
1943 my $countrydefault = $conf->config('countrydefault') || 'US';
1944 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1945 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1946 my $method = $prefix.$_;
1947 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1949 $invoice_data{'ship_country'} = ''
1950 if ( $invoice_data{'ship_country'} eq $countrydefault );
1952 $invoice_data{'cid'} = $params{'cid'}
1955 if ( $cust_main->country eq $countrydefault ) {
1956 $invoice_data{'country'} = '';
1958 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1962 $invoice_data{'address'} = \@address;
1964 $cust_main->payname.
1965 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1966 ? " (P.O. #". $cust_main->payinfo. ")"
1970 push @address, $cust_main->company
1971 if $cust_main->company;
1972 push @address, $cust_main->address1;
1973 push @address, $cust_main->address2
1974 if $cust_main->address2;
1976 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1977 push @address, $invoice_data{'country'}
1978 if $invoice_data{'country'};
1980 while (scalar(@address) < 5);
1982 $invoice_data{'logo_file'} = $params{'logo_file'}
1983 if $params{'logo_file'};
1985 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1986 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1987 #my $balance_due = $self->owed + $pr_total - $cr_total;
1988 my $balance_due = $self->owed + $pr_total;
1989 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1990 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1992 #do variable substitution in notes, footer, smallfooter
1993 foreach my $include (qw( notes footer smallfooter coupon )) {
1995 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1998 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
2000 @inc_src = $conf->config($inc_file);
2004 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2006 my $convert_map = $convert_maps{$format}{$include};
2008 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2009 s/--\@\]/$delimiters{$format}[1]/g;
2012 &$convert_map( $conf->config($inc_file) );
2016 my $inc_tt = new Text::Template (
2018 SOURCE => [ map "$_\n", @inc_src ],
2019 DELIMITERS => $delimiters{$format},
2020 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2022 unless ( $inc_tt->compile() ) {
2023 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2024 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2028 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2030 $invoice_data{$include} =~ s/\n+$//
2031 if ($format eq 'latex');
2034 $invoice_data{'po_line'} =
2035 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2036 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2039 my %money_chars = ( 'latex' => '',
2040 'html' => $conf->config('money_char') || '$',
2043 my $money_char = $money_chars{$format};
2045 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2046 'html' => $conf->config('money_char') || '$',
2049 my $other_money_char = $other_money_chars{$format};
2051 my @detail_items = ();
2052 my @total_items = ();
2056 $invoice_data{'detail_items'} = \@detail_items;
2057 $invoice_data{'total_items'} = \@total_items;
2058 $invoice_data{'buf'} = \@buf;
2059 $invoice_data{'sections'} = \@sections;
2061 my $previous_section = { 'description' => 'Previous Charges',
2062 'subtotal' => $other_money_char.
2063 sprintf('%.2f', $pr_total),
2067 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2068 'subtotal' => $taxtotal }; # adjusted below
2070 my $adjusttotal = 0;
2071 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2072 'subtotal' => 0 }; # adjusted below
2074 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2075 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2076 my $late_sections = [];
2077 if ( $multisection ) {
2078 push @sections, $self->_items_sections( $late_sections );
2080 push @sections, { 'description' => '', 'subtotal' => '' };
2083 unless ( $conf->exists('disable_previous_balance')
2084 || $conf->exists('previous_balance-summary_only')
2088 foreach my $line_item ( $self->_items_previous ) {
2091 ext_description => [],
2093 $detail->{'ref'} = $line_item->{'pkgnum'};
2094 $detail->{'quantity'} = 1;
2095 $detail->{'section'} = $previous_section;
2096 $detail->{'description'} = &$escape_function($line_item->{'description'});
2097 if ( exists $line_item->{'ext_description'} ) {
2098 @{$detail->{'ext_description'}} = map {
2099 &$escape_function($_);
2100 } @{$line_item->{'ext_description'}};
2102 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2103 $line_item->{'amount'};
2104 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2106 push @detail_items, $detail;
2107 push @buf, [ $detail->{'description'},
2108 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2114 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2115 push @buf, ['','-----------'];
2116 push @buf, [ 'Total Previous Balance',
2117 $money_char. sprintf("%10.2f", $pr_total) ];
2121 foreach my $section (@sections, @$late_sections) {
2123 $section->{'subtotal'} = $other_money_char.
2124 sprintf('%.2f', $section->{'subtotal'})
2127 if ( $section->{'description'} ) {
2128 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2134 $options{'section'} = $section if $multisection;
2135 $options{'format'} = $format;
2136 $options{'escape_function'} = $escape_function;
2137 $options{'format_function'} = sub { () } unless $unsquelched;
2138 $options{'unsquelched'} = $unsquelched;
2140 foreach my $line_item ( $self->_items_pkg(%options) ) {
2142 ext_description => [],
2144 $detail->{'ref'} = $line_item->{'pkgnum'};
2145 $detail->{'quantity'} = $line_item->{'quantity'};
2146 $detail->{'section'} = $section;
2147 $detail->{'description'} = &$escape_function($line_item->{'description'});
2148 if ( exists $line_item->{'ext_description'} ) {
2149 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2151 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2152 $line_item->{'amount'};
2153 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2154 $line_item->{'unit_amount'};
2155 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2157 push @detail_items, $detail;
2158 push @buf, ( [ $detail->{'description'},
2159 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2161 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2165 if ( $section->{'description'} ) {
2166 push @buf, ( ['','-----------'],
2167 [ $section->{'description'}. ' sub-total',
2168 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2177 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2178 unshift @sections, $previous_section if $pr_total;
2181 foreach my $tax ( $self->_items_tax ) {
2183 $taxtotal += $tax->{'amount'};
2185 my $description = &$escape_function( $tax->{'description'} );
2186 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2188 if ( $multisection ) {
2190 my $money = $old_latex ? '' : $money_char;
2191 push @detail_items, {
2192 ext_description => [],
2195 description => $description,
2196 amount => $money. $amount,
2198 section => $tax_section,
2203 push @total_items, {
2204 'total_item' => $description,
2205 'total_amount' => $other_money_char. $amount,
2210 push @buf,[ $description,
2211 $money_char. $amount,
2218 $total->{'total_item'} = 'Sub-total';
2219 $total->{'total_amount'} =
2220 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2222 if ( $multisection ) {
2223 $tax_section->{'subtotal'} = $other_money_char.
2224 sprintf('%.2f', $taxtotal);
2225 $tax_section->{'pretotal'} = 'New charges sub-total '.
2226 $total->{'total_amount'};
2227 push @sections, $tax_section if $taxtotal;
2229 unshift @total_items, $total;
2232 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2234 push @buf,['','-----------'];
2235 push @buf,[( $conf->exists('disable_previous_balance')
2237 : 'Total New Charges'
2239 $money_char. sprintf("%10.2f",$self->charged) ];
2244 $total->{'total_item'} = &$embolden_function('Total');
2245 $total->{'total_amount'} =
2246 &$embolden_function(
2249 $self->charged + ( $conf->exists('disable_previous_balance')
2255 if ( $multisection ) {
2256 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2257 sprintf('%.2f', $self->charged );
2259 push @total_items, $total;
2261 push @buf,['','-----------'];
2262 push @buf,['Total Charges',
2264 sprintf( '%10.2f', $self->charged +
2265 ( $conf->exists('disable_previous_balance')
2274 unless ( $conf->exists('disable_previous_balance') ) {
2275 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2278 my $credittotal = 0;
2279 foreach my $credit ( $self->_items_credits ) {
2281 $total->{'total_item'} = &$escape_function($credit->{'description'});
2282 $credittotal += $credit->{'amount'};
2283 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2284 $adjusttotal += $credit->{'amount'};
2285 if ( $multisection ) {
2286 my $money = $old_latex ? '' : $money_char;
2287 push @detail_items, {
2288 ext_description => [],
2291 description => &$escape_function($credit->{'description'}),
2292 amount => $money. $credit->{'amount'},
2294 section => $adjust_section,
2297 push @total_items, $total;
2300 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2303 foreach ( $self->cust_credited ) {
2305 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2307 my $reason = substr($_->cust_credit->reason,0,32);
2308 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2309 $reason = " ($reason) " if $reason;
2311 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2312 $money_char. sprintf("%10.2f",$_->amount)
2317 my $paymenttotal = 0;
2318 foreach my $payment ( $self->_items_payments ) {
2320 $total->{'total_item'} = &$escape_function($payment->{'description'});
2321 $paymenttotal += $payment->{'amount'};
2322 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2323 $adjusttotal += $payment->{'amount'};
2324 if ( $multisection ) {
2325 my $money = $old_latex ? '' : $money_char;
2326 push @detail_items, {
2327 ext_description => [],
2330 description => &$escape_function($payment->{'description'}),
2331 amount => $money. $payment->{'amount'},
2333 section => $adjust_section,
2336 push @total_items, $total;
2338 push @buf, [ $payment->{'description'},
2339 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2342 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2344 if ( $multisection ) {
2345 $adjust_section->{'subtotal'} = $other_money_char.
2346 sprintf('%.2f', $adjusttotal);
2347 push @sections, $adjust_section;
2352 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2353 $total->{'total_amount'} =
2354 &$embolden_function(
2355 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2357 if ( $multisection ) {
2358 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2359 $total->{'total_amount'};
2361 push @total_items, $total;
2363 push @buf,['','-----------'];
2364 push @buf,[$self->balance_due_msg, $money_char.
2365 sprintf("%10.2f", $balance_due ) ];
2369 if ( $multisection ) {
2370 push @sections, @$late_sections
2376 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2377 /invoice_lines\((\d*)\)/;
2378 $invoice_lines += $1 || scalar(@buf);
2381 die "no invoice_lines() functions in template?"
2382 if ( $format eq 'template' && !$wasfunc );
2384 if ($format eq 'template') {
2386 if ( $invoice_lines ) {
2387 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2388 $invoice_data{'total_pages'}++
2389 if scalar(@buf) % $invoice_lines;
2392 #setup subroutine for the template
2393 sub FS::cust_bill::_template::invoice_lines {
2394 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2396 scalar(@FS::cust_bill::_template::buf)
2397 ? shift @FS::cust_bill::_template::buf
2406 push @collect, split("\n",
2407 $text_template->fill_in( HASH => \%invoice_data,
2408 PACKAGE => 'FS::cust_bill::_template'
2411 $FS::cust_bill::_template::page++;
2413 map "$_\n", @collect;
2415 warn "filling in template for invoice ". $self->invnum. "\n"
2417 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2420 $text_template->fill_in(HASH => \%invoice_data);
2424 =item print_ps [ TIME [ , TEMPLATE ] ]
2426 Returns an postscript invoice, as a scalar.
2428 TIME an optional value used to control the printing of overdue messages. The
2429 default is now. It isn't the date of the invoice; that's the `_date' field.
2430 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2431 L<Time::Local> and L<Date::Parse> for conversion functions.
2438 my ($file, $lfile) = $self->print_latex(@_);
2439 my $ps = generate_ps($file);
2445 =item print_pdf [ TIME [ , TEMPLATE ] ]
2447 Returns an PDF invoice, as a scalar.
2449 TIME an optional value used to control the printing of overdue messages. The
2450 default is now. It isn't the date of the invoice; that's the `_date' field.
2451 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2452 L<Time::Local> and L<Date::Parse> for conversion functions.
2459 my ($file, $lfile) = $self->print_latex(@_);
2460 my $pdf = generate_pdf($file);
2466 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2468 Returns an HTML invoice, as a scalar.
2470 TIME an optional value used to control the printing of overdue messages. The
2471 default is now. It isn't the date of the invoice; that's the `_date' field.
2472 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2473 L<Time::Local> and L<Date::Parse> for conversion functions.
2475 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2476 when emailing the invoice as part of a multipart/related MIME email.
2484 %params = %{ shift() };
2486 $params{'time'} = shift;
2487 $params{'template'} = shift;
2488 $params{'cid'} = shift;
2491 $params{'format'} = 'html';
2493 $self->print_generic( %params );
2496 # quick subroutine for print_latex
2498 # There are ten characters that LaTeX treats as special characters, which
2499 # means that they do not simply typeset themselves:
2500 # # $ % & ~ _ ^ \ { }
2502 # TeX ignores blanks following an escaped character; if you want a blank (as
2503 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2507 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2508 $value =~ s/([<>])/\$$1\$/g;
2512 #utility methods for print_*
2514 sub _translate_old_latex_format {
2515 warn "_translate_old_latex_format called\n"
2522 if ( $line =~ /^%%Detail\s*$/ ) {
2524 push @template, q![@--!,
2525 q! foreach my $_tr_line (@detail_items) {!,
2526 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2527 q! $_tr_line->{'description'} .= !,
2528 q! "\\tabularnewline\n~~".!,
2529 q! join( "\\tabularnewline\n~~",!,
2530 q! @{$_tr_line->{'ext_description'}}!,
2534 while ( ( my $line_item_line = shift )
2535 !~ /^%%EndDetail\s*$/ ) {
2536 $line_item_line =~ s/'/\\'/g; # nice LTS
2537 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2538 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2539 push @template, " \$OUT .= '$line_item_line';";
2542 push @template, '}',
2545 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2547 push @template, '[@--',
2548 ' foreach my $_tr_line (@total_items) {';
2550 while ( ( my $total_item_line = shift )
2551 !~ /^%%EndTotalDetails\s*$/ ) {
2552 $total_item_line =~ s/'/\\'/g; # nice LTS
2553 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2554 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2555 push @template, " \$OUT .= '$total_item_line';";
2558 push @template, '}',
2562 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2563 push @template, $line;
2569 warn "$_\n" foreach @template;
2578 #check for an invoice- specific override (eventually)
2580 #check for a customer- specific override
2581 return $self->cust_main->invoice_terms
2582 if $self->cust_main->invoice_terms;
2584 #use configured default or default default
2585 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2591 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2592 $duedate = $self->_date() + ( $1 * 86400 );
2599 $self->due_date ? time2str(shift, $self->due_date) : '';
2602 sub balance_due_msg {
2604 my $msg = 'Balance Due';
2605 return $msg unless $self->terms;
2606 if ( $self->due_date ) {
2607 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2608 } elsif ( $self->terms ) {
2609 $msg .= ' - '. $self->terms;
2614 sub balance_due_date {
2617 if ( $conf->exists('invoice_default_terms')
2618 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2619 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2624 =item invnum_date_pretty
2626 Returns a string with the invoice number and date, for example:
2627 "Invoice #54 (3/20/2008)"
2631 sub invnum_date_pretty {
2633 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2638 Returns a string with the date, for example: "3/20/2008"
2644 time2str('%x', $self->_date);
2647 sub _items_sections {
2654 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2657 if ( $cust_bill_pkg->pkgnum > 0 ) {
2658 my $usage = $cust_bill_pkg->usage;
2660 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2661 my $desc = $display->section;
2662 my $type = $display->type;
2664 if ( $display->post_total ) {
2665 if (! $type || $type eq 'S') {
2666 $l{$desc} += $cust_bill_pkg->setup
2667 if ( $cust_bill_pkg->setup != 0 );
2671 $l{$desc} += $cust_bill_pkg->recur
2672 if ( $cust_bill_pkg->recur != 0 );
2675 if ($type && $type eq 'R') {
2676 $l{$desc} += $cust_bill_pkg->recur - $usage
2677 if ( $cust_bill_pkg->recur != 0 );
2680 if ($type && $type eq 'U') {
2681 $l{$desc} += $usage;
2685 if (! $type || $type eq 'S') {
2686 $s{$desc} += $cust_bill_pkg->setup
2687 if ( $cust_bill_pkg->setup != 0 );
2691 $s{$desc} += $cust_bill_pkg->recur
2692 if ( $cust_bill_pkg->recur != 0 );
2695 if ($type && $type eq 'R') {
2696 $s{$desc} += $cust_bill_pkg->recur - $usage
2697 if ( $cust_bill_pkg->recur != 0 );
2700 if ($type && $type eq 'U') {
2701 $s{$desc} += $usage;
2712 push @$late, map { { 'description' => $_,
2713 'subtotal' => $l{$_},
2717 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2724 #my @display = scalar(@_)
2726 # : qw( _items_previous _items_pkg );
2727 # #: qw( _items_pkg );
2728 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2729 my @display = qw( _items_previous _items_pkg );
2732 foreach my $display ( @display ) {
2733 push @b, $self->$display(@_);
2738 sub _items_previous {
2740 my $cust_main = $self->cust_main;
2741 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2743 foreach ( @pr_cust_bill ) {
2745 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2746 ' ('. time2str('%x',$_->_date). ')',
2747 #'pkgpart' => 'N/A',
2749 'amount' => sprintf("%.2f", $_->owed),
2755 # 'description' => 'Previous Balance',
2756 # #'pkgpart' => 'N/A',
2757 # 'pkgnum' => 'N/A',
2758 # 'amount' => sprintf("%10.2f", $pr_total ),
2759 # 'ext_description' => [ map {
2760 # "Invoice ". $_->invnum.
2761 # " (". time2str("%x",$_->_date). ") ".
2762 # sprintf("%10.2f", $_->owed)
2763 # } @pr_cust_bill ],
2770 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2771 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2775 return 0 unless $a cmp $b;
2776 return -1 if $b eq 'Tax';
2777 return 1 if $a eq 'Tax';
2778 return -1 if $b eq 'Other surcharges';
2779 return 1 if $a eq 'Other surcharges';
2785 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2786 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2789 sub _items_cust_bill_pkg {
2791 my $cust_bill_pkg = shift;
2794 my $format = $opt{format} || '';
2795 my $escape_function = $opt{escape_function} || sub { shift };
2796 my $format_function = $opt{format_function} || '';
2797 my $unsquelched = $opt{unsquelched} || '';
2798 my $section = $opt{section}->{description} if $opt{section};
2801 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2803 foreach my $display ( grep { defined($section)
2804 ? $_->section eq $section
2807 $cust_bill_pkg->cust_bill_pkg_display
2811 my $type = $display->type;
2813 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2815 my $desc = $cust_bill_pkg->desc;
2816 $desc = substr($desc, 0, 50). '...'
2817 if $format eq 'latex' && length($desc) > 50;
2819 my %details_opt = ( 'format' => $format,
2820 'escape_function' => $escape_function,
2821 'format_function' => $format_function,
2824 if ( $cust_bill_pkg->pkgnum > 0 ) {
2826 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2828 my $description = $desc;
2829 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2831 my @d = map &{$escape_function}($_),
2832 $cust_pkg->h_labels_short($self->_date);
2833 push @d, $cust_bill_pkg->details(%details_opt)
2834 if $cust_bill_pkg->recur == 0;
2837 description => $description,
2838 #pkgpart => $part_pkg->pkgpart,
2839 pkgnum => $cust_bill_pkg->pkgnum,
2840 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2841 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2842 quantity => $cust_bill_pkg->quantity,
2843 ext_description => \@d,
2848 if ( $cust_bill_pkg->recur != 0 &&
2849 ( !$type || $type eq 'R' || $type eq 'U' )
2853 my $is_summary = $display->summary;
2854 my $description = $is_summary ? "Usage charges" : $desc;
2856 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2857 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2858 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2861 #at least until cust_bill_pkg has "past" ranges in addition to
2862 #the "future" sdate/edate ones... see #3032
2864 push @d, map &{$escape_function}($_),
2865 $cust_pkg->h_labels_short($self->_date)
2866 #$cust_bill_pkg->edate,
2867 #$cust_bill_pkg->sdate),
2870 @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2871 push @d, $cust_bill_pkg->details(%details_opt)
2872 unless ($is_summary || $type && $type eq 'R');
2876 $amount = $cust_bill_pkg->recur;
2877 }elsif($type eq 'R') {
2878 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2879 }elsif($type eq 'U') {
2880 $amount = $cust_bill_pkg->usage;
2884 description => $description,
2885 #pkgpart => $part_pkg->pkgpart,
2886 pkgnum => $cust_bill_pkg->pkgnum,
2887 amount => sprintf("%.2f", $amount),
2888 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2889 quantity => $cust_bill_pkg->quantity,
2890 ext_description => \@d,
2891 } unless ( $type eq 'U' && ! $amount );
2895 } else { #pkgnum tax or one-shot line item (??)
2897 if ( $cust_bill_pkg->setup != 0 ) {
2899 'description' => $desc,
2900 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2903 if ( $cust_bill_pkg->recur != 0 ) {
2905 'description' => "$desc (".
2906 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2907 time2str("%x", $cust_bill_pkg->edate). ')',
2908 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2922 sub _items_credits {
2927 foreach ( $self->cust_credited ) {
2929 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2931 my $reason = $_->cust_credit->reason;
2932 #my $reason = substr($_->cust_credit->reason,0,32);
2933 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2934 $reason = " ($reason) " if $reason;
2936 #'description' => 'Credit ref\#'. $_->crednum.
2937 # " (". time2str("%x",$_->cust_credit->_date) .")".
2939 'description' => 'Credit applied '.
2940 time2str("%x",$_->cust_credit->_date). $reason,
2941 'amount' => sprintf("%.2f",$_->amount),
2944 #foreach ( @cr_cust_credit ) {
2946 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2947 # $money_char. sprintf("%10.2f",$_->credited)
2955 sub _items_payments {
2959 #get & print payments
2960 foreach ( $self->cust_bill_pay ) {
2962 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2965 'description' => "Payment received ".
2966 time2str("%x",$_->cust_pay->_date ),
2967 'amount' => sprintf("%.2f", $_->amount )
2982 =item process_reprint
2986 sub process_reprint {
2987 process_re_X('print', @_);
2990 =item process_reemail
2994 sub process_reemail {
2995 process_re_X('email', @_);
3003 process_re_X('fax', @_);
3011 process_re_X('ftp', @_);
3018 sub process_respool {
3019 process_re_X('spool', @_);
3022 use Storable qw(thaw);
3026 my( $method, $job ) = ( shift, shift );
3027 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3029 my $param = thaw(decode_base64(shift));
3030 warn Dumper($param) if $DEBUG;
3041 my($method, $job, %param ) = @_;
3043 warn "re_X $method for job $job with param:\n".
3044 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3047 #some false laziness w/search/cust_bill.html
3049 my $orderby = 'ORDER BY cust_bill._date';
3051 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3053 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3055 my @cust_bill = qsearch( {
3056 #'select' => "cust_bill.*",
3057 'table' => 'cust_bill',
3058 'addl_from' => $addl_from,
3060 'extra_sql' => $extra_sql,
3061 'order_by' => $orderby,
3065 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3067 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3070 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3071 foreach my $cust_bill ( @cust_bill ) {
3072 $cust_bill->$method();
3074 if ( $job ) { #progressbar foo
3076 if ( time - $min_sec > $last ) {
3077 my $error = $job->update_statustext(
3078 int( 100 * $num / scalar(@cust_bill) )
3080 die $error if $error;
3091 =head1 CLASS METHODS
3097 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3103 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3108 Returns an SQL fragment to retreive the net amount (charged minus credited).
3114 'charged - '. $class->credited_sql;
3119 Returns an SQL fragment to retreive the amount paid against this invoice.
3125 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3126 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3131 Returns an SQL fragment to retreive the amount credited against this invoice.
3137 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3138 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3141 =item search_sql HASHREF
3143 Class method which returns an SQL WHERE fragment to search for parameters
3144 specified in HASHREF. Valid parameters are
3150 Epoch date (UNIX timestamp) setting a lower bound for _date values
3154 Epoch date (UNIX timestamp) setting an upper bound for _date values
3168 =item newest_percust
3172 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3177 my($class, $param) = @_;
3179 warn "$me search_sql called with params: \n".
3180 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3185 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3186 push @search, "cust_bill._date >= $1";
3188 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3189 push @search, "cust_bill._date < $1";
3191 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3192 push @search, "cust_bill.invnum >= $1";
3194 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3195 push @search, "cust_bill.invnum <= $1";
3197 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3198 push @search, "cust_main.agentnum = $1";
3201 push @search, '0 != '. FS::cust_bill->owed_sql
3202 if $param->{'open'};
3204 push @search, '0 != '. FS::cust_bill->net_sql
3207 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3208 if $param->{'days'};
3210 if ( $param->{'newest_percust'} ) {
3212 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3213 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3215 my @newest_where = map { my $x = $_;
3216 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3219 grep ! /^cust_main./, @search;
3220 my $newest_where = scalar(@newest_where)
3221 ? ' AND '. join(' AND ', @newest_where)
3225 push @search, "cust_bill._date = (
3226 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3227 WHERE newest_cust_bill.custnum = cust_bill.custnum
3233 my $curuser = $FS::CurrentUser::CurrentUser;
3234 if ( $curuser->username eq 'fs_queue'
3235 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3237 my $newuser = qsearchs('access_user', {
3238 'username' => $username,
3242 $curuser = $newuser;
3244 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3248 push @search, $curuser->agentnums_sql;
3250 join(' AND ', @search );
3262 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3263 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base