4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
29 use FS::cust_bill_pay;
30 use FS::cust_bill_pay_batch;
31 use FS::part_bill_event;
34 @ISA = qw( FS::cust_main_Mixin FS::Record );
37 $me = '[FS::cust_bill]';
39 #ask FS::UID to run this stuff for us later
40 FS::UID->install_callback( sub {
42 $money_char = $conf->config('money_char') || '$';
47 FS::cust_bill - Object methods for cust_bill records
53 $record = new FS::cust_bill \%hash;
54 $record = new FS::cust_bill { 'column' => 'value' };
56 $error = $record->insert;
58 $error = $new_record->replace($old_record);
60 $error = $record->delete;
62 $error = $record->check;
64 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
66 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
68 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
70 @cust_pay_objects = $cust_bill->cust_pay;
72 $tax_amount = $record->tax;
74 @lines = $cust_bill->print_text;
75 @lines = $cust_bill->print_text $time;
79 An FS::cust_bill object represents an invoice; a declaration that a customer
80 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
81 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
82 following fields are currently supported:
86 =item invnum - primary key (assigned automatically for new invoices)
88 =item custnum - customer (see L<FS::cust_main>)
90 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
91 L<Time::Local> and L<Date::Parse> for conversion functions.
93 =item charged - amount of this invoice
95 =item printed - deprecated
97 =item closed - books closed flag, empty or `Y'
107 Creates a new invoice. To add the invoice to the database, see L<"insert">.
108 Invoices are normally created by calling the bill method of a customer object
109 (see L<FS::cust_main>).
113 sub table { 'cust_bill'; }
115 sub cust_linked { $_[0]->cust_main_custnum; }
116 sub cust_unlinked_msg {
118 "WARNING: can't find cust_main.custnum ". $self->custnum.
119 ' (cust_bill.invnum '. $self->invnum. ')';
124 Adds this invoice to the database ("Posts" the invoice). If there is an error,
125 returns the error, otherwise returns false.
129 This method now works but you probably shouldn't use it. Instead, apply a
130 credit against the invoice.
132 Using this method to delete invoices outright is really, really bad. There
133 would be no record you ever posted this invoice, and there are no check to
134 make sure charged = 0 or that there are no associated cust_bill_pkg records.
136 Really, don't use it.
142 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
143 $self->SUPER::delete(@_);
146 =item replace OLD_RECORD
148 Replaces the OLD_RECORD with this one in the database. If there is an error,
149 returns the error, otherwise returns false.
151 Only printed may be changed. printed is normally updated by calling the
152 collect method of a customer object (see L<FS::cust_main>).
156 #replace can be inherited from Record.pm
158 # replace_check is now the preferred way to #implement replace data checks
159 # (so $object->replace() works without an argument)
162 my( $new, $old ) = ( shift, shift );
163 return "Can't change custnum!" unless $old->custnum == $new->custnum;
164 #return "Can't change _date!" unless $old->_date eq $new->_date;
165 return "Can't change _date!" unless $old->_date == $new->_date;
166 return "Can't change charged!" unless $old->charged == $new->charged
167 || $old->charged == 0;
174 Checks all fields to make sure this is a valid invoice. If there is an error,
175 returns the error, otherwise returns false. Called by the insert and replace
184 $self->ut_numbern('invnum')
185 || $self->ut_number('custnum')
186 || $self->ut_numbern('_date')
187 || $self->ut_money('charged')
188 || $self->ut_numbern('printed')
189 || $self->ut_enum('closed', [ '', 'Y' ])
191 return $error if $error;
193 return "Unknown customer"
194 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
196 $self->_date(time) unless $self->_date;
198 $self->printed(0) if $self->printed eq '';
205 Returns a list consisting of the total previous balance for this customer,
206 followed by the previous outstanding invoices (as FS::cust_bill objects also).
213 my @cust_bill = sort { $a->_date <=> $b->_date }
214 grep { $_->owed != 0 && $_->_date < $self->_date }
215 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
217 foreach ( @cust_bill ) { $total += $_->owed; }
223 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
230 { 'table' => 'cust_bill_pkg',
231 'hashref' => { 'invnum' => $self->invnum },
232 'order_by' => 'ORDER BY billpkgnum',
239 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
246 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
248 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
251 =item open_cust_bill_pkg
253 Returns the open line items for this invoice.
255 Note that cust_bill_pkg with both setup and recur fees are returned as two
256 separate line items, each with only one fee.
260 # modeled after cust_main::open_cust_bill
261 sub open_cust_bill_pkg {
264 # grep { $_->owed > 0 } $self->cust_bill_pkg
266 my %other = ( 'recur' => 'setup',
267 'setup' => 'recur', );
269 foreach my $field ( qw( recur setup )) {
270 push @open, map { $_->set( $other{$field}, 0 ); $_; }
271 grep { $_->owed($field) > 0 }
272 $self->cust_bill_pkg;
278 =item cust_bill_event
280 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
284 sub cust_bill_event {
286 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
289 =item num_cust_bill_event
291 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
295 sub num_cust_bill_event {
298 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
299 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
300 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
301 $sth->fetchrow_arrayref->[0];
306 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
310 #false laziness w/cust_pkg.pm
314 'table' => 'cust_event',
315 'addl_from' => 'JOIN part_event USING ( eventpart )',
316 'hashref' => { 'tablenum' => $self->invnum },
317 'extra_sql' => " AND eventtable = 'cust_bill' ",
323 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
327 #false laziness w/cust_pkg.pm
331 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
332 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
333 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
334 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
335 $sth->fetchrow_arrayref->[0];
340 Returns the customer (see L<FS::cust_main>) for this invoice.
346 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
349 =item cust_suspend_if_balance_over AMOUNT
351 Suspends the customer associated with this invoice if the total amount owed on
352 this invoice and all older invoices is greater than the specified amount.
354 Returns a list: an empty list on success or a list of errors.
358 sub cust_suspend_if_balance_over {
359 my( $self, $amount ) = ( shift, shift );
360 my $cust_main = $self->cust_main;
361 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
364 $cust_main->suspend(@_);
370 Depreciated. See the cust_credited method.
372 #Returns a list consisting of the total previous credited (see
373 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
374 #outstanding credits (FS::cust_credit objects).
380 croak "FS::cust_bill->cust_credit depreciated; see ".
381 "FS::cust_bill->cust_credit_bill";
384 #my @cust_credit = sort { $a->_date <=> $b->_date }
385 # grep { $_->credited != 0 && $_->_date < $self->_date }
386 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
388 #foreach (@cust_credit) { $total += $_->credited; }
389 #$total, @cust_credit;
394 Depreciated. See the cust_bill_pay method.
396 #Returns all payments (see L<FS::cust_pay>) for this invoice.
402 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
404 #sort { $a->_date <=> $b->_date }
405 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
411 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
417 sort { $a->_date <=> $b->_date }
418 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
423 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
429 sort { $a->_date <=> $b->_date }
430 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
436 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
443 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
445 foreach (@taxlines) { $total += $_->setup; }
451 Returns the amount owed (still outstanding) on this invoice, which is charged
452 minus all payment applications (see L<FS::cust_bill_pay>) and credit
453 applications (see L<FS::cust_credit_bill>).
459 my $balance = $self->charged;
460 $balance -= $_->amount foreach ( $self->cust_bill_pay );
461 $balance -= $_->amount foreach ( $self->cust_credited );
462 $balance = sprintf( "%.2f", $balance);
463 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
467 =item apply_payments_and_credits
471 sub apply_payments_and_credits {
474 local $SIG{HUP} = 'IGNORE';
475 local $SIG{INT} = 'IGNORE';
476 local $SIG{QUIT} = 'IGNORE';
477 local $SIG{TERM} = 'IGNORE';
478 local $SIG{TSTP} = 'IGNORE';
479 local $SIG{PIPE} = 'IGNORE';
481 my $oldAutoCommit = $FS::UID::AutoCommit;
482 local $FS::UID::AutoCommit = 0;
485 $self->select_for_update; #mutex
487 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
488 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
490 while ( $self->owed > 0 and ( @payments || @credits ) ) {
493 if ( @payments && @credits ) {
495 #decide which goes first by weight of top (unapplied) line item
497 my @open_lineitems = $self->open_cust_bill_pkg;
500 max( map { $_->part_pkg->pay_weight || 0 }
505 my $max_credit_weight =
506 max( map { $_->part_pkg->credit_weight || 0 }
512 #if both are the same... payments first? it has to be something
513 if ( $max_pay_weight >= $max_credit_weight ) {
519 } elsif ( @payments ) {
521 } elsif ( @credits ) {
524 die "guru meditation #12 and 35";
527 if ( $app eq 'pay' ) {
529 my $payment = shift @payments;
531 $app = new FS::cust_bill_pay {
532 'paynum' => $payment->paynum,
533 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
536 } elsif ( $app eq 'credit' ) {
538 my $credit = shift @credits;
540 $app = new FS::cust_credit_bill {
541 'crednum' => $credit->crednum,
542 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
546 die "guru meditation #12 and 35";
549 $app->invnum( $self->invnum );
551 my $error = $app->insert;
553 $dbh->rollback if $oldAutoCommit;
554 return "Error inserting ". $app->table. " record: $error";
556 die $error if $error;
560 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
565 =item generate_email OPTION => VALUE ...
573 sender address, required
577 alternate template name, optional
581 text attachment arrayref, optional
585 email subject, optional
589 Returns an argument list to be passed to L<FS::Misc::send_email>.
600 my $me = '[FS::cust_bill::generate_email]';
603 'from' => $args{'from'},
604 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
607 if (ref($args{'to'}) eq 'ARRAY') {
608 $return{'to'} = $args{'to'};
610 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
611 $self->cust_main->invoicing_list
615 if ( $conf->exists('invoice_html') ) {
617 warn "$me creating HTML/text multipart message"
620 $return{'nobody'} = 1;
622 my $alternative = build MIME::Entity
623 'Type' => 'multipart/alternative',
624 'Encoding' => '7bit',
625 'Disposition' => 'inline'
629 if ( $conf->exists('invoice_email_pdf')
630 and scalar($conf->config('invoice_email_pdf_note')) ) {
632 warn "$me using 'invoice_email_pdf_note' in multipart message"
634 $data = [ map { $_ . "\n" }
635 $conf->config('invoice_email_pdf_note')
640 warn "$me not using 'invoice_email_pdf_note' in multipart message"
642 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
643 $data = $args{'print_text'};
645 $data = [ $self->print_text('', $args{'template'}) ];
650 $alternative->attach(
651 'Type' => 'text/plain',
652 #'Encoding' => 'quoted-printable',
653 'Encoding' => '7bit',
655 'Disposition' => 'inline',
658 $args{'from'} =~ /\@([\w\.\-]+)/;
659 my $from = $1 || 'example.com';
660 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
662 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
664 if ( defined($args{'template'}) && length($args{'template'})
665 && -e "$path/logo_". $args{'template'}. ".png"
668 $file = "$path/logo_". $args{'template'}. ".png";
670 $file = "$path/logo.png";
673 my $image = build MIME::Entity
674 'Type' => 'image/png',
675 'Encoding' => 'base64',
677 'Filename' => 'logo.png',
678 'Content-ID' => "<$content_id>",
681 $alternative->attach(
682 'Type' => 'text/html',
683 'Encoding' => 'quoted-printable',
684 'Data' => [ '<html>',
687 ' '. encode_entities($return{'subject'}),
690 ' <body bgcolor="#e8e8e8">',
691 $self->print_html('', $args{'template'}, $content_id),
695 'Disposition' => 'inline',
696 #'Filename' => 'invoice.pdf',
699 if ( $conf->exists('invoice_email_pdf') ) {
704 # multipart/alternative
710 my $related = build MIME::Entity 'Type' => 'multipart/related',
711 'Encoding' => '7bit';
713 #false laziness w/Misc::send_email
714 $related->head->replace('Content-type',
716 '; boundary="'. $related->head->multipart_boundary. '"'.
717 '; type=multipart/alternative'
720 $related->add_part($alternative);
722 $related->add_part($image);
724 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
726 $return{'mimeparts'} = [ $related, $pdf ];
730 #no other attachment:
732 # multipart/alternative
737 $return{'content-type'} = 'multipart/related';
738 $return{'mimeparts'} = [ $alternative, $image ];
739 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
740 #$return{'disposition'} = 'inline';
746 if ( $conf->exists('invoice_email_pdf') ) {
747 warn "$me creating PDF attachment"
750 #mime parts arguments a la MIME::Entity->build().
751 $return{'mimeparts'} = [
752 { $self->mimebuild_pdf('', $args{'template'}) }
756 if ( $conf->exists('invoice_email_pdf')
757 and scalar($conf->config('invoice_email_pdf_note')) ) {
759 warn "$me using 'invoice_email_pdf_note'"
761 $return{'body'} = [ map { $_ . "\n" }
762 $conf->config('invoice_email_pdf_note')
767 warn "$me not using 'invoice_email_pdf_note'"
769 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
770 $return{'body'} = $args{'print_text'};
772 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
785 Returns a list suitable for passing to MIME::Entity->build(), representing
786 this invoice as PDF attachment.
793 'Type' => 'application/pdf',
794 'Encoding' => 'base64',
795 'Data' => [ $self->print_pdf(@_) ],
796 'Disposition' => 'attachment',
797 'Filename' => 'invoice.pdf',
801 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
803 Sends this invoice to the destinations configured for this customer: sends
804 email, prints and/or faxes. See L<FS::cust_main_invoice>.
806 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
808 AGENTNUM, if specified, means that this invoice will only be sent for customers
809 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
810 single agent) or an arrayref of agentnums.
812 INVOICE_FROM, if specified, overrides the default email invoice From: address.
814 AMOUNT, if specified, only sends the invoice if the total amount owed on this
815 invoice and all older invoices is greater than the specified amount.
822 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
823 or die "invalid invoice number: " . $opt{invnum};
825 my @args = ( $opt{template}, $opt{agentnum} );
826 push @args, $opt{invoice_from}
827 if exists($opt{invoice_from}) && $opt{invoice_from};
829 my $error = $self->send( @args );
830 die $error if $error;
836 my $template = scalar(@_) ? shift : '';
837 if ( scalar(@_) && $_[0] ) {
838 my $agentnums = ref($_[0]) ? shift : [ shift ];
839 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
845 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
847 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
850 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
852 my @invoicing_list = $self->cust_main->invoicing_list;
854 #$self->email_invoice($template, $invoice_from)
855 $self->email($template, $invoice_from)
856 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
858 #$self->print_invoice($template)
859 $self->print($template)
860 if grep { $_ eq 'POST' } @invoicing_list; #postal
862 $self->fax_invoice($template)
863 if grep { $_ eq 'FAX' } @invoicing_list; #fax
869 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
873 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
875 INVOICE_FROM, if specified, overrides the default email invoice From: address.
879 sub queueable_email {
882 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
883 or die "invalid invoice number: " . $opt{invnum};
885 my @args = ( $opt{template} );
886 push @args, $opt{invoice_from}
887 if exists($opt{invoice_from}) && $opt{invoice_from};
889 my $error = $self->email( @args );
890 die $error if $error;
897 my $template = scalar(@_) ? shift : '';
901 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
903 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
904 $self->cust_main->invoicing_list;
906 #better to notify this person than silence
907 @invoicing_list = ($invoice_from) unless @invoicing_list;
909 my $error = send_email(
910 $self->generate_email(
911 'from' => $invoice_from,
912 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
913 'template' => $template,
916 die "can't email invoice: $error\n" if $error;
917 #die "$error\n" if $error;
921 =item lpr_data [ TEMPLATENAME ]
923 Returns the postscript or plaintext for this invoice as an arrayref.
925 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
930 my( $self, $template) = @_;
931 $conf->exists('invoice_latex')
932 ? [ $self->print_ps('', $template) ]
933 : [ $self->print_text('', $template) ];
936 =item print [ TEMPLATENAME ]
940 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
947 my $template = scalar(@_) ? shift : '';
949 do_print $self->lpr_data($template);
952 =item fax_invoice [ TEMPLATENAME ]
956 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
962 my $template = scalar(@_) ? shift : '';
964 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
965 unless $conf->exists('invoice_latex');
967 my $dialstring = $self->cust_main->getfield('fax');
970 my $error = send_fax( 'docdata' => $self->lpr_data($template),
971 'dialstring' => $dialstring,
973 die $error if $error;
977 =item ftp_invoice [ TEMPLATENAME ]
979 Sends this invoice data via FTP.
981 TEMPLATENAME is unused?
987 my $template = scalar(@_) ? shift : '';
991 'server' => $conf->config('cust_bill-ftpserver'),
992 'username' => $conf->config('cust_bill-ftpusername'),
993 'password' => $conf->config('cust_bill-ftppassword'),
994 'dir' => $conf->config('cust_bill-ftpdir'),
995 'format' => $conf->config('cust_bill-ftpformat'),
999 =item spool_invoice [ TEMPLATENAME ]
1001 Spools this invoice data (see L<FS::spool_csv>)
1003 TEMPLATENAME is unused?
1009 my $template = scalar(@_) ? shift : '';
1012 'format' => $conf->config('cust_bill-spoolformat'),
1013 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1017 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1019 Like B<send>, but only sends the invoice if it is the newest open invoice for
1024 sub send_if_newest {
1029 grep { $_->owed > 0 }
1030 qsearch('cust_bill', {
1031 'custnum' => $self->custnum,
1032 #'_date' => { op=>'>', value=>$self->_date },
1033 'invnum' => { op=>'>', value=>$self->invnum },
1040 =item send_csv OPTION => VALUE, ...
1042 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1046 protocol - currently only "ftp"
1052 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1053 and YYMMDDHHMMSS is a timestamp.
1055 See L</print_csv> for a description of the output format.
1060 my($self, %opt) = @_;
1064 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1065 mkdir $spooldir, 0700 unless -d $spooldir;
1067 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1068 my $file = "$spooldir/$tracctnum.csv";
1070 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1072 open(CSV, ">$file") or die "can't open $file: $!";
1080 if ( $opt{protocol} eq 'ftp' ) {
1081 eval "use Net::FTP;";
1083 $net = Net::FTP->new($opt{server}) or die @$;
1085 die "unknown protocol: $opt{protocol}";
1088 $net->login( $opt{username}, $opt{password} )
1089 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1091 $net->binary or die "can't set binary mode";
1093 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1095 $net->put($file) or die "can't put $file: $!";
1105 Spools CSV invoice data.
1111 =item format - 'default' or 'billco'
1113 =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>).
1115 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1117 =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.
1124 my($self, %opt) = @_;
1126 my $cust_main = $self->cust_main;
1128 if ( $opt{'dest'} ) {
1129 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1130 $cust_main->invoicing_list;
1131 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1132 || ! keys %invoicing_list;
1135 if ( $opt{'balanceover'} ) {
1137 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1140 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1141 mkdir $spooldir, 0700 unless -d $spooldir;
1143 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1147 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1148 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1151 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1153 open(CSV, ">>$file") or die "can't open $file: $!";
1154 flock(CSV, LOCK_EX);
1159 if ( lc($opt{'format'}) eq 'billco' ) {
1161 flock(CSV, LOCK_UN);
1166 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1169 open(CSV,">>$file") or die "can't open $file: $!";
1170 flock(CSV, LOCK_EX);
1176 flock(CSV, LOCK_UN);
1183 =item print_csv OPTION => VALUE, ...
1185 Returns CSV data for this invoice.
1189 format - 'default' or 'billco'
1191 Returns a list consisting of two scalars. The first is a single line of CSV
1192 header information for this invoice. The second is one or more lines of CSV
1193 detail information for this invoice.
1195 If I<format> is not specified or "default", the fields of the CSV file are as
1198 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1202 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1204 B<record_type> is C<cust_bill> for the initial header line only. The
1205 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1206 fields are filled in.
1208 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1209 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1212 =item invnum - invoice number
1214 =item custnum - customer number
1216 =item _date - invoice date
1218 =item charged - total invoice amount
1220 =item first - customer first name
1222 =item last - customer first name
1224 =item company - company name
1226 =item address1 - address line 1
1228 =item address2 - address line 1
1238 =item pkg - line item description
1240 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1242 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1244 =item sdate - start date for recurring fee
1246 =item edate - end date for recurring fee
1250 If I<format> is "billco", the fields of the header CSV file are as follows:
1252 +-------------------------------------------------------------------+
1253 | FORMAT HEADER FILE |
1254 |-------------------------------------------------------------------|
1255 | Field | Description | Name | Type | Width |
1256 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1257 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1258 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1259 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1260 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1261 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1262 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1263 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1264 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1265 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1266 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1267 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1268 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1269 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1270 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1271 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1272 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1273 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1274 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1275 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1276 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1277 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1278 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1279 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1280 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1281 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1282 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1283 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1284 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1285 +-------+-------------------------------+------------+------+-------+
1287 If I<format> is "billco", the fields of the detail CSV file are as follows:
1289 FORMAT FOR DETAIL FILE
1291 Field | Description | Name | Type | Width
1292 1 | N/A-Leave Empty | RC | CHAR | 2
1293 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1294 3 | Account Number | TRACCTNUM | CHAR | 15
1295 4 | Invoice Number | TRINVOICE | CHAR | 15
1296 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1297 6 | Transaction Detail | DETAILS | CHAR | 100
1298 7 | Amount | AMT | NUM* | 9
1299 8 | Line Format Control** | LNCTRL | CHAR | 2
1300 9 | Grouping Code | GROUP | CHAR | 2
1301 10 | User Defined | ACCT CODE | CHAR | 15
1306 my($self, %opt) = @_;
1308 eval "use Text::CSV_XS";
1311 my $cust_main = $self->cust_main;
1313 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1315 if ( lc($opt{'format'}) eq 'billco' ) {
1318 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1320 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1322 my( $previous_balance, @unused ) = $self->previous; #previous balance
1324 my $pmt_cr_applied = 0;
1325 $pmt_cr_applied += $_->{'amount'}
1326 foreach ( $self->_items_payments, $self->_items_credits ) ;
1328 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1331 '', # 1 | N/A-Leave Empty CHAR 2
1332 '', # 2 | N/A-Leave Empty CHAR 15
1333 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1334 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1335 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1336 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1337 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1338 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1339 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1340 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1341 '', # 10 | Ancillary Billing Information CHAR 30
1342 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1343 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1346 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1349 $duedate, # 14 | Bill Due Date CHAR 10
1351 $previous_balance, # 15 | Previous Balance NUM* 9
1352 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1353 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1354 $totaldue, # 18 | Total Amt Due NUM* 9
1355 $totaldue, # 19 | Total Amt Due NUM* 9
1356 '', # 20 | 30 Day Aging NUM* 9
1357 '', # 21 | 60 Day Aging NUM* 9
1358 '', # 22 | 90 Day Aging NUM* 9
1359 'N', # 23 | Y/N CHAR 1
1360 '', # 24 | Remittance automation CHAR 100
1361 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1362 $self->custnum, # 26 | Customer Reference Number CHAR 15
1363 '0', # 27 | Federal Tax*** NUM* 9
1364 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1365 '0', # 29 | Other Taxes & Fees*** NUM* 9
1374 time2str("%x", $self->_date),
1375 sprintf("%.2f", $self->charged),
1376 ( map { $cust_main->getfield($_) }
1377 qw( first last company address1 address2 city state zip country ) ),
1379 ) or die "can't create csv";
1382 my $header = $csv->string. "\n";
1385 if ( lc($opt{'format'}) eq 'billco' ) {
1388 foreach my $item ( $self->_items_pkg ) {
1391 '', # 1 | N/A-Leave Empty CHAR 2
1392 '', # 2 | N/A-Leave Empty CHAR 15
1393 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1394 $self->invnum, # 4 | Invoice Number CHAR 15
1395 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1396 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1397 $item->{'amount'}, # 7 | Amount NUM* 9
1398 '', # 8 | Line Format Control** CHAR 2
1399 '', # 9 | Grouping Code CHAR 2
1400 '', # 10 | User Defined CHAR 15
1403 $detail .= $csv->string. "\n";
1409 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1411 my($pkg, $setup, $recur, $sdate, $edate);
1412 if ( $cust_bill_pkg->pkgnum ) {
1414 ($pkg, $setup, $recur, $sdate, $edate) = (
1415 $cust_bill_pkg->part_pkg->pkg,
1416 ( $cust_bill_pkg->setup != 0
1417 ? sprintf("%.2f", $cust_bill_pkg->setup )
1419 ( $cust_bill_pkg->recur != 0
1420 ? sprintf("%.2f", $cust_bill_pkg->recur )
1422 ( $cust_bill_pkg->sdate
1423 ? time2str("%x", $cust_bill_pkg->sdate)
1425 ($cust_bill_pkg->edate
1426 ?time2str("%x", $cust_bill_pkg->edate)
1430 } else { #pkgnum tax
1431 next unless $cust_bill_pkg->setup != 0;
1432 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1433 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1435 ($pkg, $setup, $recur, $sdate, $edate) =
1436 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1442 ( map { '' } (1..11) ),
1443 ($pkg, $setup, $recur, $sdate, $edate)
1444 ) or die "can't create csv";
1446 $detail .= $csv->string. "\n";
1452 ( $header, $detail );
1458 Pays this invoice with a compliemntary payment. If there is an error,
1459 returns the error, otherwise returns false.
1465 my $cust_pay = new FS::cust_pay ( {
1466 'invnum' => $self->invnum,
1467 'paid' => $self->owed,
1470 'payinfo' => $self->cust_main->payinfo,
1478 Attempts to pay this invoice with a credit card payment via a
1479 Business::OnlinePayment realtime gateway. See
1480 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1481 for supported processors.
1487 $self->realtime_bop( 'CC', @_ );
1492 Attempts to pay this invoice with an electronic check (ACH) payment via a
1493 Business::OnlinePayment realtime gateway. See
1494 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1495 for supported processors.
1501 $self->realtime_bop( 'ECHECK', @_ );
1506 Attempts to pay this invoice with phone bill (LEC) payment via a
1507 Business::OnlinePayment realtime gateway. See
1508 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1509 for supported processors.
1515 $self->realtime_bop( 'LEC', @_ );
1519 my( $self, $method ) = @_;
1521 my $cust_main = $self->cust_main;
1522 my $balance = $cust_main->balance;
1523 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1524 $amount = sprintf("%.2f", $amount);
1525 return "not run (balance $balance)" unless $amount > 0;
1527 my $description = 'Internet Services';
1528 if ( $conf->exists('business-onlinepayment-description') ) {
1529 my $dtempl = $conf->config('business-onlinepayment-description');
1531 my $agent_obj = $cust_main->agent
1532 or die "can't retreive agent for $cust_main (agentnum ".
1533 $cust_main->agentnum. ")";
1534 my $agent = $agent_obj->agent;
1535 my $pkgs = join(', ',
1536 map { $_->part_pkg->pkg }
1537 grep { $_->pkgnum } $self->cust_bill_pkg
1539 $description = eval qq("$dtempl");
1542 $cust_main->realtime_bop($method, $amount,
1543 'description' => $description,
1544 'invnum' => $self->invnum,
1549 =item batch_card OPTION => VALUE...
1551 Adds a payment for this invoice to the pending credit card batch (see
1552 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1553 runs the payment using a realtime gateway.
1558 my ($self, %options) = @_;
1559 my $cust_main = $self->cust_main;
1561 $options{invnum} = $self->invnum;
1563 $cust_main->batch_card(%options);
1566 sub _agent_template {
1568 $self->cust_main->agent_template;
1571 sub _agent_invoice_from {
1573 $self->cust_main->agent_invoice_from;
1576 =item print_text [ TIME [ , TEMPLATE ] ]
1578 Returns an text invoice, as a list of lines.
1580 TIME an optional value used to control the printing of overdue messages. The
1581 default is now. It isn't the date of the invoice; that's the `_date' field.
1582 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1583 L<Time::Local> and L<Date::Parse> for conversion functions.
1588 my( $self, $today, $template ) = @_;
1590 my %params = ( 'format' => 'template' );
1591 $params{'time'} = $today if $today;
1592 $params{'template'} = $template if $template;
1594 $self->print_generic( %params );
1597 =item print_latex [ TIME [ , TEMPLATE ] ]
1599 Internal method - returns a filename of a filled-in LaTeX template for this
1600 invoice (Note: add ".tex" to get the actual filename), and a filename of
1601 an associated logo (with the .eps extension included).
1603 See print_ps and print_pdf for methods that return PostScript and PDF output.
1605 TIME an optional value used to control the printing of overdue messages. The
1606 default is now. It isn't the date of the invoice; that's the `_date' field.
1607 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1608 L<Time::Local> and L<Date::Parse> for conversion functions.
1614 my( $self, $today, $template ) = @_;
1616 my %params = ( 'format' => 'latex' );
1617 $params{'time'} = $today if $today;
1618 $params{'template'} = $template if $template;
1620 $template ||= $self->_agent_template;
1622 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1623 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1627 ) or die "can't open temp file: $!\n";
1629 if ($template && $conf->exists("logo_${template}.eps")) {
1630 print $lh $conf->config_binary("logo_${template}.eps")
1631 or die "can't write temp file: $!\n";
1633 print $lh $conf->config_binary('logo.eps')
1634 or die "can't write temp file: $!\n";
1637 $params{'logo_file'} = $lh->filename;
1639 my @filled_in = $self->print_generic( %params );
1641 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1645 ) or die "can't open temp file: $!\n";
1646 print $fh join('', @filled_in );
1649 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1650 return ($1, $params{'logo_file'});
1654 =item print_generic OPTIONS_HASH
1656 Internal method - returns a filled-in template for this invoice as a scalar.
1658 See print_ps and print_pdf for methods that return PostScript and PDF output.
1660 Non optional options include
1661 format - latex, html, template
1663 Optional options include
1665 template - a value used as a suffix for a configuration template
1667 time - a value used to control the printing of overdue messages. The
1668 default is now. It isn't the date of the invoice; that's the `_date' field.
1669 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1670 L<Time::Local> and L<Date::Parse> for conversion functions.
1674 unsquelch_cdr - overrides any per customer cdr squelching when true
1680 my( $self, %params ) = @_;
1681 my $today = $params{today} ? $params{today} : time;
1682 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1685 my $format = $params{format};
1686 die "Unknown format: $format"
1687 unless $format =~ /^(latex|html|template)$/;
1689 my $cust_main = $self->cust_main;
1690 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1691 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1694 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1695 'html' => [ '<%=', '%>' ],
1696 'template' => [ '{', '}' ],
1699 #create the template
1700 my $template = $params{template} ? $params{template} : $self->_agent_template;
1701 my $templatefile = "invoice_$format";
1702 $templatefile .= "_$template"
1703 if length($template);
1704 my @invoice_template = map "$_\n", $conf->config($templatefile)
1705 or die "cannot load config data $templatefile";
1708 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1709 #change this to a die when the old code is removed
1710 warn "old-style invoice template $templatefile; ".
1711 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1712 $old_latex = 'true';
1713 @invoice_template = _translate_old_latex_format(@invoice_template);
1716 my $text_template = new Text::Template(
1718 SOURCE => \@invoice_template,
1719 DELIMITERS => $delimiters{$format},
1722 $text_template->compile()
1723 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1726 # additional substitution could possibly cause breakage in existing templates
1727 my %convert_maps = (
1729 'notes' => sub { map "$_", @_ },
1730 'footer' => sub { map "$_", @_ },
1731 'smallfooter' => sub { map "$_", @_ },
1732 'returnaddress' => sub { map "$_", @_ },
1733 'coupon' => sub { map "$_", @_ },
1739 s/%%(.*)$/<!-- $1 -->/g;
1740 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1741 s/\\begin\{enumerate\}/<ol>/g;
1743 s/\\end\{enumerate\}/<\/ol>/g;
1744 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1753 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1755 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1760 s/\\\\\*?\s*$/<BR>/;
1761 s/\\hyphenation\{[\w\s\-]+}//;
1765 'coupon' => sub { "" },
1772 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1773 s/\\begin\{enumerate\}//g;
1775 s/\\end\{enumerate\}//g;
1776 s/\\textbf\{(.*)\}/$1/g;
1783 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1785 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1790 s/\\\\\*?\s*$/\n/; # dubious
1791 s/\\hyphenation\{[\w\s\-]+}//;
1795 'coupon' => sub { "" },
1800 # hashes for differing output formats
1801 my %nbsps = ( 'latex' => '~',
1802 'html' => '', # '&nbps;' would be nice
1803 'template' => '', # not used
1805 my $nbsp = $nbsps{$format};
1807 my %escape_functions = ( 'latex' => \&_latex_escape,
1808 'html' => \&encode_entities,
1809 'template' => sub { shift },
1811 my $escape_function = $escape_functions{$format};
1813 my %date_formats = ( 'latex' => '%b %o, %Y',
1814 'html' => '%b %o, %Y',
1817 my $date_format = $date_formats{$format};
1819 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1821 'html' => sub { return '<b>'. shift(). '</b>'
1823 'template' => sub { shift },
1825 my $embolden_function = $embolden_functions{$format};
1828 # generate template variables
1831 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1835 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1841 $returnaddress = join("\n",
1842 $conf->config_orbase("invoice_${format}returnaddress", $template)
1845 } elsif ( grep /\S/,
1846 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1848 my $convert_map = $convert_maps{$format}{'returnaddress'};
1851 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1856 } elsif ( grep /\S/, $conf->config('company_address') ) {
1858 my $convert_map = $convert_maps{$format}{'returnaddress'};
1859 $returnaddress = join( "\n", &$convert_map(
1860 map { s/( {2,})/'~' x length($1)/eg;
1864 ( $conf->config('company_name'),
1865 $conf->config('company_address'),
1872 my $warning = "Couldn't find a return address; ".
1873 "do you need to set the company_address configuration value?";
1875 $returnaddress = $nbsp;
1876 #$returnaddress = $warning;
1880 my %invoice_data = (
1881 'company_name' => scalar( $conf->config('company_name') ),
1882 'company_address' => join("\n", $conf->config('company_address') ). "\n",
1883 'custnum' => $self->custnum,
1884 'invnum' => $self->invnum,
1885 'date' => time2str($date_format, $self->_date),
1886 'today' => time2str('%b %o, %Y', $today),
1887 'agent' => &$escape_function($cust_main->agent->agent),
1888 'agent_custid' => &$escape_function($cust_main->agent_custid),
1889 'payname' => &$escape_function($cust_main->payname),
1890 'company' => &$escape_function($cust_main->company),
1891 'address1' => &$escape_function($cust_main->address1),
1892 'address2' => &$escape_function($cust_main->address2),
1893 'city' => &$escape_function($cust_main->city),
1894 'state' => &$escape_function($cust_main->state),
1895 'zip' => &$escape_function($cust_main->zip),
1896 'returnaddress' => $returnaddress,
1898 'terms' => $self->terms,
1899 'template' => $params{'template'},
1900 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1901 # better hang on to conf_dir for a while
1902 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1905 'current_charges' => sprintf("%.2f", $self->charged),
1906 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1907 'ship_enable' => $conf->exists('invoice-ship_address'),
1908 'unitprices' => $conf->exists('invoice-unitprice'),
1911 my $countrydefault = $conf->config('countrydefault') || 'US';
1912 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1913 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1914 my $method = $prefix.$_;
1915 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1917 $invoice_data{'ship_country'} = ''
1918 if ( $invoice_data{'ship_country'} eq $countrydefault );
1920 $invoice_data{'cid'} = $params{'cid'}
1923 if ( $cust_main->country eq $countrydefault ) {
1924 $invoice_data{'country'} = '';
1926 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1930 $invoice_data{'address'} = \@address;
1932 $cust_main->payname.
1933 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1934 ? " (P.O. #". $cust_main->payinfo. ")"
1938 push @address, $cust_main->company
1939 if $cust_main->company;
1940 push @address, $cust_main->address1;
1941 push @address, $cust_main->address2
1942 if $cust_main->address2;
1944 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1945 push @address, $invoice_data{'country'}
1946 if $invoice_data{'country'};
1948 while (scalar(@address) < 5);
1950 $invoice_data{'logo_file'} = $params{'logo_file'}
1951 if $params{'logo_file'};
1953 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1954 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1955 #my $balance_due = $self->owed + $pr_total - $cr_total;
1956 my $balance_due = $self->owed + $pr_total;
1957 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1958 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1960 #do variable substitution in notes, footer, smallfooter
1961 foreach my $include (qw( notes footer smallfooter coupon )) {
1963 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
1966 if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
1968 @inc_src = $conf->config($inc_file);
1972 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
1974 my $convert_map = $convert_maps{$format}{$include};
1976 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
1977 s/--\@\]/$delimiters{$format}[1]/g;
1980 &$convert_map( $conf->config($inc_file) );
1984 my $inc_tt = new Text::Template (
1986 SOURCE => [ map "$_\n", @inc_src ],
1987 DELIMITERS => $delimiters{$format},
1988 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1990 unless ( $inc_tt->compile() ) {
1991 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
1992 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1996 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1998 $invoice_data{$include} =~ s/\n+$//
1999 if ($format eq 'latex');
2002 $invoice_data{'po_line'} =
2003 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2004 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2007 my %money_chars = ( 'latex' => '',
2008 'html' => $conf->config('money_char') || '$',
2011 my $money_char = $money_chars{$format};
2013 my %other_money_chars = ( 'latex' => '\dollar ',
2014 'html' => $conf->config('money_char') || '$',
2017 my $other_money_char = $other_money_chars{$format};
2019 my @detail_items = ();
2020 my @total_items = ();
2024 $invoice_data{'detail_items'} = \@detail_items;
2025 $invoice_data{'total_items'} = \@total_items;
2026 $invoice_data{'buf'} = \@buf;
2027 $invoice_data{'sections'} = \@sections;
2029 my $previous_section = { 'description' => 'Previous Charges',
2030 'subtotal' => $other_money_char.
2031 sprintf('%.2f', $pr_total),
2035 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2036 'subtotal' => $taxtotal }; # adjusted below
2038 my $adjusttotal = 0;
2039 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2040 'subtotal' => 0 }; # adjusted below
2042 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2043 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2044 my $late_sections = [];
2045 if ( $multisection ) {
2046 push @sections, $self->_items_sections( $late_sections );
2048 push @sections, { 'description' => '', 'subtotal' => '' };
2051 foreach my $line_item ( $conf->exists('disable_previous_balance')
2053 : $self->_items_previous
2057 ext_description => [],
2059 $detail->{'ref'} = $line_item->{'pkgnum'};
2060 $detail->{'quantity'} = 1;
2061 $detail->{'section'} = $previous_section;
2062 $detail->{'description'} = &$escape_function($line_item->{'description'});
2063 if ( exists $line_item->{'ext_description'} ) {
2064 @{$detail->{'ext_description'}} = map {
2065 &$escape_function($_);
2066 } @{$line_item->{'ext_description'}};
2068 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2069 $line_item->{'amount'};
2070 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2072 push @detail_items, $detail;
2073 push @buf, [ $detail->{'description'},
2074 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2078 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2079 push @buf, ['','-----------'];
2080 push @buf, [ 'Total Previous Balance',
2081 $money_char. sprintf("%10.2f", $pr_total) ];
2085 foreach my $section (@sections, @$late_sections) {
2087 $section->{'subtotal'} = $other_money_char.
2088 sprintf('%.2f', $section->{'subtotal'})
2091 if ( $section->{'description'} ) {
2092 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2098 $options{'section'} = $section if $multisection;
2099 $options{'format'} = $format;
2100 $options{'escape_function'} = $escape_function;
2101 $options{'format_function'} = sub { () } unless $unsquelched;
2102 $options{'unsquelched'} = $unsquelched;
2104 foreach my $line_item ( $self->_items_pkg(%options) ) {
2106 ext_description => [],
2108 $detail->{'ref'} = $line_item->{'pkgnum'};
2109 $detail->{'quantity'} = $line_item->{'quantity'};
2110 $detail->{'section'} = $section;
2111 $detail->{'description'} = &$escape_function($line_item->{'description'});
2112 if ( exists $line_item->{'ext_description'} ) {
2113 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2115 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2116 $line_item->{'amount'};
2117 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2118 $line_item->{'unit_amount'};
2119 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2121 push @detail_items, $detail;
2122 push @buf, ( [ $detail->{'description'},
2123 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2125 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2129 if ( $section->{'description'} ) {
2130 push @buf, ( ['','-----------'],
2131 [ $section->{'description'}. ' sub-total',
2132 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2141 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2142 unshift @sections, $previous_section if $pr_total;
2145 foreach my $tax ( $self->_items_tax ) {
2147 $total->{'total_item'} = &$escape_function($tax->{'description'});
2148 $taxtotal += $tax->{'amount'};
2149 $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
2150 if ( $multisection ) {
2151 my $money = $old_latex ? '' : $money_char;
2152 push @detail_items, {
2153 ext_description => [],
2156 description => &$escape_function($tax->{'description'}),
2157 amount => $money. $tax->{'amount'},
2159 section => $tax_section,
2162 push @total_items, $total;
2164 push @buf,[ $total->{'total_item'},
2165 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
2172 $total->{'total_item'} = 'Sub-total';
2173 $total->{'total_amount'} =
2174 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2176 if ( $multisection ) {
2177 $tax_section->{'subtotal'} = $other_money_char.
2178 sprintf('%.2f', $taxtotal);
2179 $tax_section->{'pretotal'} = 'New charges sub-total '.
2180 $total->{'total_amount'};
2181 push @sections, $tax_section if $taxtotal;
2183 unshift @total_items, $total;
2186 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2188 push @buf,['','-----------'];
2189 push @buf,[( $conf->exists('disable_previous_balance')
2191 : 'Total New Charges'
2193 $money_char. sprintf("%10.2f",$self->charged) ];
2198 $total->{'total_item'} = &$embolden_function('Total');
2199 $total->{'total_amount'} =
2200 &$embolden_function(
2203 $self->charged + ( $conf->exists('disable_previous_balance')
2209 if ( $multisection ) {
2210 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2211 sprintf('%.2f', $self->charged );
2213 push @total_items, $total;
2215 push @buf,['','-----------'];
2216 push @buf,['Total Charges',
2218 sprintf( '%10.2f', $self->charged +
2219 ( $conf->exists('disable_previous_balance')
2228 unless ( $conf->exists('disable_previous_balance') ) {
2229 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2232 my $credittotal = 0;
2233 foreach my $credit ( $self->_items_credits ) {
2235 $total->{'total_item'} = &$escape_function($credit->{'description'});
2236 $credittotal += $credit->{'amount'};
2237 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2238 $adjusttotal += $credit->{'amount'};
2239 if ( $multisection ) {
2240 my $money = $old_latex ? '' : $money_char;
2241 push @detail_items, {
2242 ext_description => [],
2245 description => &$escape_function($credit->{'description'}),
2246 amount => $money. $credit->{'amount'},
2248 section => $adjust_section,
2251 push @total_items, $total;
2254 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2257 foreach ( $self->cust_credited ) {
2259 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2261 my $reason = substr($_->cust_credit->reason,0,32);
2262 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2263 $reason = " ($reason) " if $reason;
2265 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2266 $money_char. sprintf("%10.2f",$_->amount)
2271 my $paymenttotal = 0;
2272 foreach my $payment ( $self->_items_payments ) {
2274 $total->{'total_item'} = &$escape_function($payment->{'description'});
2275 $paymenttotal += $payment->{'amount'};
2276 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2277 $adjusttotal += $payment->{'amount'};
2278 if ( $multisection ) {
2279 my $money = $old_latex ? '' : $money_char;
2280 push @detail_items, {
2281 ext_description => [],
2284 description => &$escape_function($payment->{'description'}),
2285 amount => $money. $payment->{'amount'},
2287 section => $adjust_section,
2290 push @total_items, $total;
2292 push @buf, [ $payment->{'description'},
2293 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2296 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2298 if ( $multisection ) {
2299 $adjust_section->{'subtotal'} = $other_money_char.
2300 sprintf('%.2f', $adjusttotal);
2301 push @sections, $adjust_section;
2306 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2307 $total->{'total_amount'} =
2308 &$embolden_function(
2309 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2311 if ( $multisection ) {
2312 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2313 $total->{'total_amount'};
2315 push @total_items, $total;
2317 push @buf,['','-----------'];
2318 push @buf,[$self->balance_due_msg, $money_char.
2319 sprintf("%10.2f", $balance_due ) ];
2323 if ( $multisection ) {
2324 push @sections, @$late_sections
2330 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2331 /invoice_lines\((\d*)\)/;
2332 $invoice_lines += $1 || scalar(@buf);
2335 die "no invoice_lines() functions in template?"
2336 if ( $format eq 'template' && !$wasfunc );
2338 if ($format eq 'template') {
2340 if ( $invoice_lines ) {
2341 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2342 $invoice_data{'total_pages'}++
2343 if scalar(@buf) % $invoice_lines;
2346 #setup subroutine for the template
2347 sub FS::cust_bill::_template::invoice_lines {
2348 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2350 scalar(@FS::cust_bill::_template::buf)
2351 ? shift @FS::cust_bill::_template::buf
2360 push @collect, split("\n",
2361 $text_template->fill_in( HASH => \%invoice_data,
2362 PACKAGE => 'FS::cust_bill::_template'
2365 $FS::cust_bill::_template::page++;
2367 map "$_\n", @collect;
2369 warn "filling in template for invoice ". $self->invnum. "\n"
2371 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2374 $text_template->fill_in(HASH => \%invoice_data);
2378 =item print_ps [ TIME [ , TEMPLATE ] ]
2380 Returns an postscript invoice, as a scalar.
2382 TIME an optional value used to control the printing of overdue messages. The
2383 default is now. It isn't the date of the invoice; that's the `_date' field.
2384 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2385 L<Time::Local> and L<Date::Parse> for conversion functions.
2392 my ($file, $lfile) = $self->print_latex(@_);
2393 my $ps = generate_ps($file);
2399 =item print_pdf [ TIME [ , TEMPLATE ] ]
2401 Returns an PDF invoice, as a scalar.
2403 TIME an optional value used to control the printing of overdue messages. The
2404 default is now. It isn't the date of the invoice; that's the `_date' field.
2405 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2406 L<Time::Local> and L<Date::Parse> for conversion functions.
2413 my ($file, $lfile) = $self->print_latex(@_);
2414 my $pdf = generate_pdf($file);
2420 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2422 Returns an HTML invoice, as a scalar.
2424 TIME an optional value used to control the printing of overdue messages. The
2425 default is now. It isn't the date of the invoice; that's the `_date' field.
2426 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2427 L<Time::Local> and L<Date::Parse> for conversion functions.
2429 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2430 when emailing the invoice as part of a multipart/related MIME email.
2438 %params = %{ shift() };
2440 $params{'time'} = shift;
2441 $params{'template'} = shift;
2442 $params{'cid'} = shift;
2445 $params{'format'} = 'html';
2447 $self->print_generic( %params );
2450 # quick subroutine for print_latex
2452 # There are ten characters that LaTeX treats as special characters, which
2453 # means that they do not simply typeset themselves:
2454 # # $ % & ~ _ ^ \ { }
2456 # TeX ignores blanks following an escaped character; if you want a blank (as
2457 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2461 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2462 $value =~ s/([<>])/\$$1\$/g;
2466 #utility methods for print_*
2468 sub _translate_old_latex_format {
2469 warn "_translate_old_latex_format called\n"
2476 if ( $line =~ /^%%Detail\s*$/ ) {
2478 push @template, q![@--!,
2479 q! foreach my $_tr_line (@detail_items) {!,
2480 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2481 q! $_tr_line->{'description'} .= !,
2482 q! "\\tabularnewline\n~~".!,
2483 q! join( "\\tabularnewline\n~~",!,
2484 q! @{$_tr_line->{'ext_description'}}!,
2488 while ( ( my $line_item_line = shift )
2489 !~ /^%%EndDetail\s*$/ ) {
2490 $line_item_line =~ s/'/\\'/g; # nice LTS
2491 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2492 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2493 push @template, " \$OUT .= '$line_item_line';";
2496 push @template, '}',
2499 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2501 push @template, '[@--',
2502 ' foreach my $_tr_line (@total_items) {';
2504 while ( ( my $total_item_line = shift )
2505 !~ /^%%EndTotalDetails\s*$/ ) {
2506 $total_item_line =~ s/'/\\'/g; # nice LTS
2507 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2508 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2509 push @template, " \$OUT .= '$total_item_line';";
2512 push @template, '}',
2516 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2517 push @template, $line;
2523 warn "$_\n" foreach @template;
2532 #check for an invoice- specific override (eventually)
2534 #check for a customer- specific override
2535 return $self->cust_main->invoice_terms
2536 if $self->cust_main->invoice_terms;
2538 #use configured default or default default
2539 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2545 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2546 $duedate = $self->_date() + ( $1 * 86400 );
2553 $self->due_date ? time2str(shift, $self->due_date) : '';
2556 sub balance_due_msg {
2558 my $msg = 'Balance Due';
2559 return $msg unless $self->terms;
2560 if ( $self->due_date ) {
2561 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2562 } elsif ( $self->terms ) {
2563 $msg .= ' - '. $self->terms;
2568 sub balance_due_date {
2571 if ( $conf->exists('invoice_default_terms')
2572 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2573 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2578 =item invnum_date_pretty
2580 Returns a string with the invoice number and date, for example:
2581 "Invoice #54 (3/20/2008)"
2585 sub invnum_date_pretty {
2587 'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
2590 sub _items_sections {
2597 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2600 if ( $cust_bill_pkg->pkgnum > 0 ) {
2602 my $desc = $cust_bill_pkg->section;
2603 my $dup_desc = $cust_bill_pkg->duplicate_section;
2605 if ($cust_bill_pkg->duplicate) {
2606 $s{$dup_desc} += $cust_bill_pkg->setup
2607 if ( $cust_bill_pkg->setup != 0 );
2609 $s{$dup_desc} += $cust_bill_pkg->recur
2610 if ( $cust_bill_pkg->recur != 0 );
2613 if ( $cust_bill_pkg->post_total ) {
2614 $l{$desc} += $cust_bill_pkg->setup
2615 if ( $cust_bill_pkg->setup != 0 );
2617 $l{$desc} += $cust_bill_pkg->recur
2618 if ( $cust_bill_pkg->recur != 0 );
2621 $s{$desc} += $cust_bill_pkg->setup
2622 if ( $cust_bill_pkg->setup != 0 );
2624 $s{$desc} += $cust_bill_pkg->recur
2625 if ( $cust_bill_pkg->recur != 0 );
2632 push @$late, map { { 'description' => $_,
2633 'subtotal' => $l{$_},
2637 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2644 #my @display = scalar(@_)
2646 # : qw( _items_previous _items_pkg );
2647 # #: qw( _items_pkg );
2648 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2649 my @display = qw( _items_previous _items_pkg );
2652 foreach my $display ( @display ) {
2653 push @b, $self->$display(@_);
2658 sub _items_previous {
2660 my $cust_main = $self->cust_main;
2661 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2663 foreach ( @pr_cust_bill ) {
2665 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2666 ' ('. time2str('%x',$_->_date). ')',
2667 #'pkgpart' => 'N/A',
2669 'amount' => sprintf("%.2f", $_->owed),
2675 # 'description' => 'Previous Balance',
2676 # #'pkgpart' => 'N/A',
2677 # 'pkgnum' => 'N/A',
2678 # 'amount' => sprintf("%10.2f", $pr_total ),
2679 # 'ext_description' => [ map {
2680 # "Invoice ". $_->invnum.
2681 # " (". time2str("%x",$_->_date). ") ".
2682 # sprintf("%10.2f", $_->owed)
2683 # } @pr_cust_bill ],
2691 my $section = $options{'section'};
2692 my $desc = $section->{'description'};
2694 grep { $_->pkgnum &&
2696 ? ( $_->section eq $desc || $_->duplicate_section eq $desc )
2699 } $self->cust_bill_pkg;
2700 $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
2704 return 0 unless $a cmp $b;
2705 return -1 if $b eq 'Tax';
2706 return 1 if $a eq 'Tax';
2707 return -1 if $b eq 'Other surcharges';
2708 return 1 if $a eq 'Other surcharges';
2714 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2715 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2718 sub _items_cust_bill_pkg {
2720 my $cust_bill_pkg = shift;
2723 my $format = $opt{format} || '';
2724 my $escape_function = $opt{escape_function} || sub { shift };
2725 my $format_function = $opt{format_function} || '';
2726 my $unsquelched = $opt{unsquelched} || '';
2729 my $last_pkgnum = '';
2730 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2733 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2735 my $desc = $cust_bill_pkg->desc;
2737 my %details_opt = ( 'format' => $format,
2738 'escape_function' => $escape_function,
2739 'format_function' => $format_function,
2742 if ( $cust_bill_pkg->pkgnum > 0 ) {
2744 if ( $cust_bill_pkg->setup != 0 ) {
2746 my $description = $desc;
2747 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2749 my @d = map &{$escape_function}($_),
2750 $cust_pkg->h_labels_short($self->_date);
2751 push @d, $cust_bill_pkg->details(%details_opt)
2752 if $cust_bill_pkg->recur == 0;
2755 description => $description,
2756 #pkgpart => $part_pkg->pkgpart,
2757 pkgnum => $cust_bill_pkg->pkgnum,
2758 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2759 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2760 quantity => $cust_bill_pkg->quantity,
2761 ext_description => \@d,
2768 if ( $cust_bill_pkg->recur != 0 ) {
2771 ( $cust_bill_pkg->duplicate &&
2772 $opt{section}->{description} ne $cust_bill_pkg->section
2774 my $description = $is_summary ? "Usage charges" : $desc;
2776 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2777 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2778 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2781 #at least until cust_bill_pkg has "past" ranges in addition to
2782 #the "future" sdate/edate ones... see #3032
2784 push @d, map &{$escape_function}($_),
2785 $cust_pkg->h_labels_short($self->_date)
2786 #$cust_bill_pkg->edate,
2787 #$cust_bill_pkg->sdate),
2788 unless ($cust_bill_pkg->pkgnum eq $last_pkgnum);
2790 @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
2791 push @d, $cust_bill_pkg->details(%details_opt)
2794 if ($cust_bill_pkg->pkgnum eq $last_pkgnum) {
2797 sprintf("%.2f", $b[$#b]->{amount} + $cust_bill_pkg->recur);
2798 push @{$b[$#b]->{ext_description}}, @d;
2803 description => $description,
2804 #pkgpart => $part_pkg->pkgpart,
2805 pkgnum => $cust_bill_pkg->pkgnum,
2806 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2807 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2808 quantity => $cust_bill_pkg->quantity,
2809 ext_description => \@d,
2814 if ($conf->exists('separate_usage') && $cust_bill_pkg->type ne 'U') {
2817 $last_pkgnum = $cust_bill_pkg->pkgnum;
2821 } else { #pkgnum tax or one-shot line item (??)
2823 if ( $cust_bill_pkg->setup != 0 ) {
2825 'description' => $desc,
2826 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2829 if ( $cust_bill_pkg->recur != 0 ) {
2831 'description' => "$desc (".
2832 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2833 time2str("%x", $cust_bill_pkg->edate). ')',
2834 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2848 sub _items_credits {
2853 foreach ( $self->cust_credited ) {
2855 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2857 my $reason = $_->cust_credit->reason;
2858 #my $reason = substr($_->cust_credit->reason,0,32);
2859 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2860 $reason = " ($reason) " if $reason;
2862 #'description' => 'Credit ref\#'. $_->crednum.
2863 # " (". time2str("%x",$_->cust_credit->_date) .")".
2865 'description' => 'Credit applied '.
2866 time2str("%x",$_->cust_credit->_date). $reason,
2867 'amount' => sprintf("%.2f",$_->amount),
2870 #foreach ( @cr_cust_credit ) {
2872 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2873 # $money_char. sprintf("%10.2f",$_->credited)
2881 sub _items_payments {
2885 #get & print payments
2886 foreach ( $self->cust_bill_pay ) {
2888 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2891 'description' => "Payment received ".
2892 time2str("%x",$_->cust_pay->_date ),
2893 'amount' => sprintf("%.2f", $_->amount )
2908 =item process_reprint
2912 sub process_reprint {
2913 process_re_X('print', @_);
2916 =item process_reemail
2920 sub process_reemail {
2921 process_re_X('email', @_);
2929 process_re_X('fax', @_);
2937 process_re_X('ftp', @_);
2944 sub process_respool {
2945 process_re_X('spool', @_);
2948 use Storable qw(thaw);
2952 my( $method, $job ) = ( shift, shift );
2953 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2955 my $param = thaw(decode_base64(shift));
2956 warn Dumper($param) if $DEBUG;
2967 my($method, $job, %param ) = @_;
2969 warn "re_X $method for job $job with param:\n".
2970 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2973 #some false laziness w/search/cust_bill.html
2975 my $orderby = 'ORDER BY cust_bill._date';
2977 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2979 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2981 my @cust_bill = qsearch( {
2982 #'select' => "cust_bill.*",
2983 'table' => 'cust_bill',
2984 'addl_from' => $addl_from,
2986 'extra_sql' => $extra_sql,
2987 'order_by' => $orderby,
2991 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2993 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2996 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2997 foreach my $cust_bill ( @cust_bill ) {
2998 $cust_bill->$method();
3000 if ( $job ) { #progressbar foo
3002 if ( time - $min_sec > $last ) {
3003 my $error = $job->update_statustext(
3004 int( 100 * $num / scalar(@cust_bill) )
3006 die $error if $error;
3017 =head1 CLASS METHODS
3023 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3029 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3034 Returns an SQL fragment to retreive the net amount (charged minus credited).
3040 'charged - '. $class->credited_sql;
3045 Returns an SQL fragment to retreive the amount paid against this invoice.
3051 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3052 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3057 Returns an SQL fragment to retreive the amount credited against this invoice.
3063 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3064 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3067 =item search_sql HASHREF
3069 Class method which returns an SQL WHERE fragment to search for parameters
3070 specified in HASHREF. Valid parameters are
3076 Epoch date (UNIX timestamp) setting a lower bound for _date values
3080 Epoch date (UNIX timestamp) setting an upper bound for _date values
3094 =item newest_percust
3098 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3103 my($class, $param) = @_;
3105 warn "$me search_sql called with params: \n".
3106 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3111 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3112 push @search, "cust_bill._date >= $1";
3114 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3115 push @search, "cust_bill._date < $1";
3117 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3118 push @search, "cust_bill.invnum >= $1";
3120 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3121 push @search, "cust_bill.invnum <= $1";
3123 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3124 push @search, "cust_main.agentnum = $1";
3127 push @search, '0 != '. FS::cust_bill->owed_sql
3128 if $param->{'open'};
3130 push @search, '0 != '. FS::cust_bill->net_sql
3133 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3134 if $param->{'days'};
3136 if ( $param->{'newest_percust'} ) {
3138 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3139 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3141 my @newest_where = map { my $x = $_;
3142 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3145 grep ! /^cust_main./, @search;
3146 my $newest_where = scalar(@newest_where)
3147 ? ' AND '. join(' AND ', @newest_where)
3151 push @search, "cust_bill._date = (
3152 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3153 WHERE newest_cust_bill.custnum = cust_bill.custnum
3159 my $curuser = $FS::CurrentUser::CurrentUser;
3160 if ( $curuser->username eq 'fs_queue'
3161 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3163 my $newuser = qsearchs('access_user', {
3164 'username' => $username,
3168 $curuser = $newuser;
3170 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3174 push @search, $curuser->agentnums_sql;
3176 join(' AND ', @search );
3188 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3189 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base