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;
28 use FS::cust_bill_pay;
29 use FS::cust_bill_pay_batch;
30 use FS::part_bill_event;
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
36 $me = '[FS::cust_bill]';
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub {
41 $money_char = $conf->config('money_char') || '$';
46 FS::cust_bill - Object methods for cust_bill records
52 $record = new FS::cust_bill \%hash;
53 $record = new FS::cust_bill { 'column' => 'value' };
55 $error = $record->insert;
57 $error = $new_record->replace($old_record);
59 $error = $record->delete;
61 $error = $record->check;
63 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
65 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
67 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
69 @cust_pay_objects = $cust_bill->cust_pay;
71 $tax_amount = $record->tax;
73 @lines = $cust_bill->print_text;
74 @lines = $cust_bill->print_text $time;
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
81 following fields are currently supported:
85 =item invnum - primary key (assigned automatically for new invoices)
87 =item custnum - customer (see L<FS::cust_main>)
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
92 =item charged - amount of this invoice
94 =item printed - deprecated
96 =item closed - books closed flag, empty or `Y'
106 Creates a new invoice. To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
112 sub table { 'cust_bill'; }
114 sub cust_linked { $_[0]->cust_main_custnum; }
115 sub cust_unlinked_msg {
117 "WARNING: can't find cust_main.custnum ". $self->custnum.
118 ' (cust_bill.invnum '. $self->invnum. ')';
123 Adds this invoice to the database ("Posts" the invoice). If there is an error,
124 returns the error, otherwise returns false.
128 This method now works but you probably shouldn't use it. Instead, apply a
129 credit against the invoice.
131 Using this method to delete invoices outright is really, really bad. There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
135 Really, don't use it.
141 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
143 local $SIG{HUP} = 'IGNORE';
144 local $SIG{INT} = 'IGNORE';
145 local $SIG{QUIT} = 'IGNORE';
146 local $SIG{TERM} = 'IGNORE';
147 local $SIG{TSTP} = 'IGNORE';
148 local $SIG{PIPE} = 'IGNORE';
150 my $oldAutoCommit = $FS::UID::AutoCommit;
151 local $FS::UID::AutoCommit = 0;
154 foreach my $table (qw(
165 foreach my $linked ( $self->$table() ) {
166 my $error = $linked->delete;
168 $dbh->rollback if $oldAutoCommit;
175 my $error = $self->SUPER::delete(@_);
177 $dbh->rollback if $oldAutoCommit;
181 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
187 =item replace OLD_RECORD
189 Replaces the OLD_RECORD with this one in the database. If there is an error,
190 returns the error, otherwise returns false.
192 Only printed may be changed. printed is normally updated by calling the
193 collect method of a customer object (see L<FS::cust_main>).
197 #replace can be inherited from Record.pm
199 # replace_check is now the preferred way to #implement replace data checks
200 # (so $object->replace() works without an argument)
203 my( $new, $old ) = ( shift, shift );
204 return "Can't change custnum!" unless $old->custnum == $new->custnum;
205 #return "Can't change _date!" unless $old->_date eq $new->_date;
206 return "Can't change _date!" unless $old->_date == $new->_date;
207 return "Can't change charged!" unless $old->charged == $new->charged
208 || $old->charged == 0;
215 Checks all fields to make sure this is a valid invoice. If there is an error,
216 returns the error, otherwise returns false. Called by the insert and replace
225 $self->ut_numbern('invnum')
226 || $self->ut_number('custnum')
227 || $self->ut_numbern('_date')
228 || $self->ut_money('charged')
229 || $self->ut_numbern('printed')
230 || $self->ut_enum('closed', [ '', 'Y' ])
232 return $error if $error;
234 return "Unknown customer"
235 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
237 $self->_date(time) unless $self->_date;
239 $self->printed(0) if $self->printed eq '';
246 Returns a list consisting of the total previous balance for this customer,
247 followed by the previous outstanding invoices (as FS::cust_bill objects also).
254 my @cust_bill = sort { $a->_date <=> $b->_date }
255 grep { $_->owed != 0 && $_->_date < $self->_date }
256 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
258 foreach ( @cust_bill ) { $total += $_->owed; }
264 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
270 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
275 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
282 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
284 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
287 =item open_cust_bill_pkg
289 Returns the open line items for this invoice.
291 Note that cust_bill_pkg with both setup and recur fees are returned as two
292 separate line items, each with only one fee.
296 # modeled after cust_main::open_cust_bill
297 sub open_cust_bill_pkg {
300 # grep { $_->owed > 0 } $self->cust_bill_pkg
302 my %other = ( 'recur' => 'setup',
303 'setup' => 'recur', );
305 foreach my $field ( qw( recur setup )) {
306 push @open, map { $_->set( $other{$field}, 0 ); $_; }
307 grep { $_->owed($field) > 0 }
308 $self->cust_bill_pkg;
314 =item cust_bill_event
316 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
321 sub cust_bill_event {
323 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
329 Returns the customer (see L<FS::cust_main>) for this invoice.
335 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
338 =item cust_suspend_if_balance_over AMOUNT
340 Suspends the customer associated with this invoice if the total amount owed on
341 this invoice and all older invoices is greater than the specified amount.
343 Returns a list: an empty list on success or a list of errors.
347 sub cust_suspend_if_balance_over {
348 my( $self, $amount ) = ( shift, shift );
349 my $cust_main = $self->cust_main;
350 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
353 $cust_main->suspend(@_);
359 Depreciated. See the cust_credited method.
361 #Returns a list consisting of the total previous credited (see
362 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
363 #outstanding credits (FS::cust_credit objects).
369 croak "FS::cust_bill->cust_credit depreciated; see ".
370 "FS::cust_bill->cust_credit_bill";
373 #my @cust_credit = sort { $a->_date <=> $b->_date }
374 # grep { $_->credited != 0 && $_->_date < $self->_date }
375 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
377 #foreach (@cust_credit) { $total += $_->credited; }
378 #$total, @cust_credit;
383 Depreciated. See the cust_bill_pay method.
385 #Returns all payments (see L<FS::cust_pay>) for this invoice.
391 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
393 #sort { $a->_date <=> $b->_date }
394 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
400 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
403 sub cust_bill_pay_batch {
405 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
410 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
416 sort { $a->_date <=> $b->_date }
417 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
422 =item cust_credit_bill
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 } )
435 sub cust_credit_bill {
436 shift->cust_credited(@_);
441 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
448 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
450 foreach (@taxlines) { $total += $_->setup; }
456 Returns the amount owed (still outstanding) on this invoice, which is charged
457 minus all payment applications (see L<FS::cust_bill_pay>) and credit
458 applications (see L<FS::cust_credit_bill>).
464 my $balance = $self->charged;
465 $balance -= $_->amount foreach ( $self->cust_bill_pay );
466 $balance -= $_->amount foreach ( $self->cust_credited );
467 $balance = sprintf( "%.2f", $balance);
468 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
472 =item apply_payments_and_credits
476 sub apply_payments_and_credits {
479 local $SIG{HUP} = 'IGNORE';
480 local $SIG{INT} = 'IGNORE';
481 local $SIG{QUIT} = 'IGNORE';
482 local $SIG{TERM} = 'IGNORE';
483 local $SIG{TSTP} = 'IGNORE';
484 local $SIG{PIPE} = 'IGNORE';
486 my $oldAutoCommit = $FS::UID::AutoCommit;
487 local $FS::UID::AutoCommit = 0;
490 $self->select_for_update; #mutex
492 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
493 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
495 while ( $self->owed > 0 and ( @payments || @credits ) ) {
498 if ( @payments && @credits ) {
500 #decide which goes first by weight of top (unapplied) line item
502 my @open_lineitems = $self->open_cust_bill_pkg;
505 max( map { $_->part_pkg->pay_weight || 0 }
510 my $max_credit_weight =
511 max( map { $_->part_pkg->credit_weight || 0 }
517 #if both are the same... payments first? it has to be something
518 if ( $max_pay_weight >= $max_credit_weight ) {
524 } elsif ( @payments ) {
526 } elsif ( @credits ) {
529 die "guru meditation #12 and 35";
532 if ( $app eq 'pay' ) {
534 my $payment = shift @payments;
536 $app = new FS::cust_bill_pay {
537 'paynum' => $payment->paynum,
538 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
541 } elsif ( $app eq 'credit' ) {
543 my $credit = shift @credits;
545 $app = new FS::cust_credit_bill {
546 'crednum' => $credit->crednum,
547 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
551 die "guru meditation #12 and 35";
554 $app->invnum( $self->invnum );
556 my $error = $app->insert;
558 $dbh->rollback if $oldAutoCommit;
559 return "Error inserting ". $app->table. " record: $error";
561 die $error if $error;
565 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
570 =item generate_email PARAMHASH
572 PARAMHASH can contain the following:
576 =item from => sender address, required
578 =item tempate => alternate template name, optional
580 =item print_text => text attachment arrayref, optional
582 =item subject => email subject, optional
586 Returns an argument list to be passed to L<FS::Misc::send_email>.
597 my $me = '[FS::cust_bill::generate_email]';
600 'from' => $args{'from'},
601 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
604 if (ref($args{'to'}) eq 'ARRAY') {
605 $return{'to'} = $args{'to'};
607 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
608 $self->cust_main->invoicing_list
612 if ( $conf->exists('invoice_html') ) {
614 warn "$me creating HTML/text multipart message"
617 $return{'nobody'} = 1;
619 my $alternative = build MIME::Entity
620 'Type' => 'multipart/alternative',
621 'Encoding' => '7bit',
622 'Disposition' => 'inline'
626 if ( $conf->exists('invoice_email_pdf')
627 and scalar($conf->config('invoice_email_pdf_note')) ) {
629 warn "$me using 'invoice_email_pdf_note' in multipart message"
631 $data = [ map { $_ . "\n" }
632 $conf->config('invoice_email_pdf_note')
637 warn "$me not using 'invoice_email_pdf_note' in multipart message"
639 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
640 $data = $args{'print_text'};
642 $data = [ $self->print_text('', $args{'template'}) ];
647 $alternative->attach(
648 'Type' => 'text/plain',
649 #'Encoding' => 'quoted-printable',
650 'Encoding' => '7bit',
652 'Disposition' => 'inline',
655 $args{'from'} =~ /\@([\w\.\-]+)/;
656 my $from = $1 || 'example.com';
657 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
659 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
661 if ( defined($args{'template'}) && length($args{'template'})
662 && -e "$path/logo_". $args{'template'}. ".png"
665 $file = "$path/logo_". $args{'template'}. ".png";
667 $file = "$path/logo.png";
670 my $image = build MIME::Entity
671 'Type' => 'image/png',
672 'Encoding' => 'base64',
674 'Filename' => 'logo.png',
675 'Content-ID' => "<$content_id>",
678 $alternative->attach(
679 'Type' => 'text/html',
680 'Encoding' => 'quoted-printable',
681 'Data' => [ '<html>',
684 ' '. encode_entities($return{'subject'}),
687 ' <body bgcolor="#e8e8e8">',
688 $self->print_html('', $args{'template'}, $content_id),
692 'Disposition' => 'inline',
693 #'Filename' => 'invoice.pdf',
696 if ( $conf->exists('invoice_email_pdf') ) {
701 # multipart/alternative
707 my $related = build MIME::Entity 'Type' => 'multipart/related',
708 'Encoding' => '7bit';
710 #false laziness w/Misc::send_email
711 $related->head->replace('Content-type',
713 '; boundary="'. $related->head->multipart_boundary. '"'.
714 '; type=multipart/alternative'
717 $related->add_part($alternative);
719 $related->add_part($image);
721 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
723 $return{'mimeparts'} = [ $related, $pdf ];
727 #no other attachment:
729 # multipart/alternative
734 $return{'content-type'} = 'multipart/related';
735 $return{'mimeparts'} = [ $alternative, $image ];
736 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
737 #$return{'disposition'} = 'inline';
743 if ( $conf->exists('invoice_email_pdf') ) {
744 warn "$me creating PDF attachment"
747 #mime parts arguments a la MIME::Entity->build().
748 $return{'mimeparts'} = [
749 { $self->mimebuild_pdf('', $args{'template'}) }
753 if ( $conf->exists('invoice_email_pdf')
754 and scalar($conf->config('invoice_email_pdf_note')) ) {
756 warn "$me using 'invoice_email_pdf_note'"
758 $return{'body'} = [ map { $_ . "\n" }
759 $conf->config('invoice_email_pdf_note')
764 warn "$me not using 'invoice_email_pdf_note'"
766 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
767 $return{'body'} = $args{'print_text'};
769 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
782 Returns a list suitable for passing to MIME::Entity->build(), representing
783 this invoice as PDF attachment.
790 'Type' => 'application/pdf',
791 'Encoding' => 'base64',
792 'Data' => [ $self->print_pdf(@_) ],
793 'Disposition' => 'attachment',
794 'Filename' => 'invoice-'. $self->invnum. '.pdf',
798 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
800 Sends this invoice to the destinations configured for this customer: sends
801 email, prints and/or faxes. See L<FS::cust_main_invoice>.
803 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
805 AGENTNUM, if specified, means that this invoice will only be sent for customers
806 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
807 single agent) or an arrayref of agentnums.
809 INVOICE_FROM, if specified, overrides the default email invoice From: address.
811 AMOUNT, if specified, only sends the invoice if the total amount owed on this
812 invoice and all older invoices is greater than the specified amount.
819 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
820 or die "invalid invoice number: " . $opt{invnum};
822 my @args = ( $opt{template}, $opt{agentnum} );
823 push @args, $opt{invoice_from}
824 if exists($opt{invoice_from}) && $opt{invoice_from};
826 my $error = $self->send( @args );
827 die $error if $error;
833 my $template = scalar(@_) ? shift : '';
834 if ( scalar(@_) && $_[0] ) {
835 my $agentnums = ref($_[0]) ? shift : [ shift ];
836 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
842 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
844 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
847 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
849 my @invoicing_list = $self->cust_main->invoicing_list;
851 #$self->email_invoice($template, $invoice_from)
852 $self->email($template, $invoice_from)
853 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
855 #$self->print_invoice($template)
856 $self->print($template)
857 if grep { $_ eq 'POST' } @invoicing_list; #postal
859 $self->fax_invoice($template)
860 if grep { $_ eq 'FAX' } @invoicing_list; #fax
866 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
870 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
872 INVOICE_FROM, if specified, overrides the default email invoice From: address.
876 sub queueable_email {
879 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
880 or die "invalid invoice number: " . $opt{invnum};
882 my @args = ( $opt{template} );
883 push @args, $opt{invoice_from}
884 if exists($opt{invoice_from}) && $opt{invoice_from};
886 my $error = $self->email( @args );
887 die $error if $error;
894 my $template = scalar(@_) ? shift : '';
898 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
900 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
901 $self->cust_main->invoicing_list;
903 #better to notify this person than silence
904 @invoicing_list = ($invoice_from) unless @invoicing_list;
906 my $subject = $self->email_subject($template);
908 my $error = send_email(
909 $self->generate_email(
910 'from' => $invoice_from,
911 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
912 'subject' => $subject,
913 'template' => $template,
916 die "can't email invoice: $error\n" if $error;
917 #die "$error\n" if $error;
924 #my $template = scalar(@_) ? shift : '';
927 my $subject = $conf->config('invoice_subject') || 'Invoice';
929 my $cust_main = $self->cust_main;
930 my $name = $cust_main->name;
931 my $name_short = $cust_main->name_short;
932 my $invoice_number = $self->invnum;
933 my $invoice_date = $self->_date_pretty;
938 =item lpr_data [ TEMPLATENAME ]
940 Returns the postscript or plaintext for this invoice as an arrayref.
942 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
947 my( $self, $template) = @_;
948 $conf->exists('invoice_latex')
949 ? [ $self->print_ps('', $template) ]
950 : [ $self->print_text('', $template) ];
953 =item print [ TEMPLATENAME ]
957 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
964 my $template = scalar(@_) ? shift : '';
966 do_print $self->lpr_data($template);
969 =item fax_invoice [ TEMPLATENAME ]
973 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
979 my $template = scalar(@_) ? shift : '';
981 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
982 unless $conf->exists('invoice_latex');
984 my $dialstring = $self->cust_main->getfield('fax');
987 my $error = send_fax( 'docdata' => $self->lpr_data($template),
988 'dialstring' => $dialstring,
990 die $error if $error;
994 =item ftp_invoice [ TEMPLATENAME ]
996 Sends this invoice data via FTP.
998 TEMPLATENAME is unused?
1004 my $template = scalar(@_) ? shift : '';
1007 'protocol' => 'ftp',
1008 'server' => $conf->config('cust_bill-ftpserver'),
1009 'username' => $conf->config('cust_bill-ftpusername'),
1010 'password' => $conf->config('cust_bill-ftppassword'),
1011 'dir' => $conf->config('cust_bill-ftpdir'),
1012 'format' => $conf->config('cust_bill-ftpformat'),
1016 =item spool_invoice [ TEMPLATENAME ]
1018 Spools this invoice data (see L<FS::spool_csv>)
1020 TEMPLATENAME is unused?
1026 my $template = scalar(@_) ? shift : '';
1029 'format' => $conf->config('cust_bill-spoolformat'),
1030 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1034 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1036 Like B<send>, but only sends the invoice if it is the newest open invoice for
1041 sub send_if_newest {
1046 grep { $_->owed > 0 }
1047 qsearch('cust_bill', {
1048 'custnum' => $self->custnum,
1049 #'_date' => { op=>'>', value=>$self->_date },
1050 'invnum' => { op=>'>', value=>$self->invnum },
1057 =item send_csv OPTION => VALUE, ...
1059 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1063 protocol - currently only "ftp"
1069 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1070 and YYMMDDHHMMSS is a timestamp.
1072 See L</print_csv> for a description of the output format.
1077 my($self, %opt) = @_;
1081 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1082 mkdir $spooldir, 0700 unless -d $spooldir;
1084 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1085 my $file = "$spooldir/$tracctnum.csv";
1087 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1089 open(CSV, ">$file") or die "can't open $file: $!";
1097 if ( $opt{protocol} eq 'ftp' ) {
1098 eval "use Net::FTP;";
1100 $net = Net::FTP->new($opt{server}) or die @$;
1102 die "unknown protocol: $opt{protocol}";
1105 $net->login( $opt{username}, $opt{password} )
1106 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1108 $net->binary or die "can't set binary mode";
1110 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1112 $net->put($file) or die "can't put $file: $!";
1122 Spools CSV invoice data.
1128 =item format - 'default' or 'billco'
1130 =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>).
1132 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1134 =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.
1141 my($self, %opt) = @_;
1143 my $cust_main = $self->cust_main;
1145 if ( $opt{'dest'} ) {
1146 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1147 $cust_main->invoicing_list;
1148 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1149 || ! keys %invoicing_list;
1152 if ( $opt{'balanceover'} ) {
1154 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1157 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1158 mkdir $spooldir, 0700 unless -d $spooldir;
1160 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1164 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1165 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1168 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1170 open(CSV, ">>$file") or die "can't open $file: $!";
1171 flock(CSV, LOCK_EX);
1176 if ( lc($opt{'format'}) eq 'billco' ) {
1178 flock(CSV, LOCK_UN);
1183 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1186 open(CSV,">>$file") or die "can't open $file: $!";
1187 flock(CSV, LOCK_EX);
1193 flock(CSV, LOCK_UN);
1200 =item print_csv OPTION => VALUE, ...
1202 Returns CSV data for this invoice.
1206 format - 'default' or 'billco'
1208 Returns a list consisting of two scalars. The first is a single line of CSV
1209 header information for this invoice. The second is one or more lines of CSV
1210 detail information for this invoice.
1212 If I<format> is not specified or "default", the fields of the CSV file are as
1215 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1219 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1221 B<record_type> is C<cust_bill> for the initial header line only. The
1222 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1223 fields are filled in.
1225 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1226 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1229 =item invnum - invoice number
1231 =item custnum - customer number
1233 =item _date - invoice date
1235 =item charged - total invoice amount
1237 =item first - customer first name
1239 =item last - customer first name
1241 =item company - company name
1243 =item address1 - address line 1
1245 =item address2 - address line 1
1255 =item pkg - line item description
1257 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1259 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1261 =item sdate - start date for recurring fee
1263 =item edate - end date for recurring fee
1267 If I<format> is "billco", the fields of the header CSV file are as follows:
1269 +-------------------------------------------------------------------+
1270 | FORMAT HEADER FILE |
1271 |-------------------------------------------------------------------|
1272 | Field | Description | Name | Type | Width |
1273 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1274 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1275 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1276 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1277 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1278 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1279 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1280 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1281 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1282 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1283 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1284 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1285 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1286 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1287 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1288 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1289 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1290 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1291 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1292 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1293 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1294 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1295 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1296 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1297 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1298 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1299 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1300 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1301 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1302 +-------+-------------------------------+------------+------+-------+
1304 If I<format> is "billco", the fields of the detail CSV file are as follows:
1306 FORMAT FOR DETAIL FILE
1308 Field | Description | Name | Type | Width
1309 1 | N/A-Leave Empty | RC | CHAR | 2
1310 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1311 3 | Account Number | TRACCTNUM | CHAR | 15
1312 4 | Invoice Number | TRINVOICE | CHAR | 15
1313 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1314 6 | Transaction Detail | DETAILS | CHAR | 100
1315 7 | Amount | AMT | NUM* | 9
1316 8 | Line Format Control** | LNCTRL | CHAR | 2
1317 9 | Grouping Code | GROUP | CHAR | 2
1318 10 | User Defined | ACCT CODE | CHAR | 15
1323 my($self, %opt) = @_;
1325 eval "use Text::CSV_XS";
1328 my $cust_main = $self->cust_main;
1330 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1332 if ( lc($opt{'format'}) eq 'billco' ) {
1335 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1337 my $duedate = $self->balance_due_date;
1339 my( $previous_balance, @unused ) = $self->previous; #previous balance
1341 my $pmt_cr_applied = 0;
1342 $pmt_cr_applied += $_->{'amount'}
1343 foreach ( $self->_items_payments, $self->_items_credits ) ;
1345 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1348 '', # 1 | N/A-Leave Empty CHAR 2
1349 '', # 2 | N/A-Leave Empty CHAR 15
1350 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1351 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1352 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1353 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1354 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1355 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1356 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1357 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1358 '', # 10 | Ancillary Billing Information CHAR 30
1359 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1360 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1363 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1366 $duedate, # 14 | Bill Due Date CHAR 10
1368 $previous_balance, # 15 | Previous Balance NUM* 9
1369 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1370 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1371 $totaldue, # 18 | Total Amt Due NUM* 9
1372 $totaldue, # 19 | Total Amt Due NUM* 9
1373 '', # 20 | 30 Day Aging NUM* 9
1374 '', # 21 | 60 Day Aging NUM* 9
1375 '', # 22 | 90 Day Aging NUM* 9
1376 'N', # 23 | Y/N CHAR 1
1377 '', # 24 | Remittance automation CHAR 100
1378 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1379 $self->custnum, # 26 | Customer Reference Number CHAR 15
1380 '0', # 27 | Federal Tax*** NUM* 9
1381 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1382 '0', # 29 | Other Taxes & Fees*** NUM* 9
1391 time2str("%x", $self->_date),
1392 sprintf("%.2f", $self->charged),
1393 ( map { $cust_main->getfield($_) }
1394 qw( first last company address1 address2 city state zip country ) ),
1396 ) or die "can't create csv";
1399 my $header = $csv->string. "\n";
1402 if ( lc($opt{'format'}) eq 'billco' ) {
1405 foreach my $item ( $self->_items_pkg ) {
1408 '', # 1 | N/A-Leave Empty CHAR 2
1409 '', # 2 | N/A-Leave Empty CHAR 15
1410 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1411 $self->invnum, # 4 | Invoice Number CHAR 15
1412 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1413 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1414 $item->{'amount'}, # 7 | Amount NUM* 9
1415 '', # 8 | Line Format Control** CHAR 2
1416 '', # 9 | Grouping Code CHAR 2
1417 '', # 10 | User Defined CHAR 15
1420 $detail .= $csv->string. "\n";
1426 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1428 my($pkg, $setup, $recur, $sdate, $edate);
1429 if ( $cust_bill_pkg->pkgnum ) {
1431 ($pkg, $setup, $recur, $sdate, $edate) = (
1432 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1433 ( $cust_bill_pkg->setup != 0
1434 ? sprintf("%.2f", $cust_bill_pkg->setup )
1436 ( $cust_bill_pkg->recur != 0
1437 ? sprintf("%.2f", $cust_bill_pkg->recur )
1439 ( $cust_bill_pkg->sdate
1440 ? time2str("%x", $cust_bill_pkg->sdate)
1442 ($cust_bill_pkg->edate
1443 ?time2str("%x", $cust_bill_pkg->edate)
1447 } else { #pkgnum tax
1448 next unless $cust_bill_pkg->setup != 0;
1449 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1450 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1452 ($pkg, $setup, $recur, $sdate, $edate) =
1453 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1459 ( map { '' } (1..11) ),
1460 ($pkg, $setup, $recur, $sdate, $edate)
1461 ) or die "can't create csv";
1463 $detail .= $csv->string. "\n";
1469 ( $header, $detail );
1475 Pays this invoice with a compliemntary payment. If there is an error,
1476 returns the error, otherwise returns false.
1482 my $cust_pay = new FS::cust_pay ( {
1483 'invnum' => $self->invnum,
1484 'paid' => $self->owed,
1487 'payinfo' => $self->cust_main->payinfo,
1495 Attempts to pay this invoice with a credit card payment via a
1496 Business::OnlinePayment realtime gateway. See
1497 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1498 for supported processors.
1504 $self->realtime_bop( 'CC', @_ );
1509 Attempts to pay this invoice with an electronic check (ACH) payment via a
1510 Business::OnlinePayment realtime gateway. See
1511 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1512 for supported processors.
1518 $self->realtime_bop( 'ECHECK', @_ );
1523 Attempts to pay this invoice with phone bill (LEC) payment via a
1524 Business::OnlinePayment realtime gateway. See
1525 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1526 for supported processors.
1532 $self->realtime_bop( 'LEC', @_ );
1536 my( $self, $method ) = @_;
1538 my $cust_main = $self->cust_main;
1539 my $balance = $cust_main->balance;
1540 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1541 $amount = sprintf("%.2f", $amount);
1542 return "not run (balance $balance)" unless $amount > 0;
1544 my $description = 'Internet Services';
1545 if ( $conf->exists('business-onlinepayment-description') ) {
1546 my $dtempl = $conf->config('business-onlinepayment-description');
1548 my $agent_obj = $cust_main->agent
1549 or die "can't retreive agent for $cust_main (agentnum ".
1550 $cust_main->agentnum. ")";
1551 my $agent = $agent_obj->agent;
1552 my $pkgs = join(', ',
1553 map { $_->cust_pkg->part_pkg->pkg }
1554 grep { $_->pkgnum } $self->cust_bill_pkg
1556 $description = eval qq("$dtempl");
1559 $cust_main->realtime_bop($method, $amount,
1560 'description' => $description,
1561 'invnum' => $self->invnum,
1566 =item batch_card OPTION => VALUE...
1568 Adds a payment for this invoice to the pending credit card batch (see
1569 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1570 runs the payment using a realtime gateway.
1575 my ($self, %options) = @_;
1576 my $cust_main = $self->cust_main;
1578 $options{invnum} = $self->invnum;
1580 $cust_main->batch_card(%options);
1583 sub _agent_template {
1585 $self->cust_main->agent_template;
1588 sub _agent_invoice_from {
1590 $self->cust_main->agent_invoice_from;
1593 =item print_text [ TIME [ , TEMPLATE ] ]
1595 Returns an text invoice, as a list of lines.
1597 TIME an optional value used to control the printing of overdue messages. The
1598 default is now. It isn't the date of the invoice; that's the `_date' field.
1599 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1600 L<Time::Local> and L<Date::Parse> for conversion functions.
1604 #still some false laziness w/_items stuff (and send_csv)
1607 my( $self, $today, $template ) = @_;
1610 # my $invnum = $self->invnum;
1611 my $cust_main = $self->cust_main;
1612 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1613 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1615 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1616 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1617 #my $balance_due = $self->owed + $pr_total - $cr_total;
1618 my $balance_due = $self->owed + $pr_total;
1621 #my($description,$amount);
1625 unless ($conf->exists('disable_previous_balance')) {
1626 foreach ( @pr_cust_bill ) {
1628 "Previous Balance, Invoice #". $_->invnum.
1629 " (". time2str("%x",$_->_date). ")",
1630 $money_char. sprintf("%10.2f",$_->owed)
1633 if (@pr_cust_bill) {
1634 push @buf,['','-----------'];
1635 push @buf,[ 'Total Previous Balance',
1636 $money_char. sprintf("%10.2f",$pr_total ) ];
1642 foreach my $cust_bill_pkg (
1643 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1644 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1647 my $desc = $cust_bill_pkg->desc;
1649 if ( $cust_bill_pkg->pkgnum > 0 ) {
1651 if ( $cust_bill_pkg->setup != 0 ) {
1652 my $description = $desc;
1653 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1654 push @buf, [ $description,
1655 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1657 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1658 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1661 if ( $cust_bill_pkg->recur != 0 ) {
1664 ( $conf->exists('disable_line_item_date_ranges')
1666 : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1667 time2str("%x", $cust_bill_pkg->edate) . ")"
1669 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1672 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1673 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1674 $cust_bill_pkg->sdate );
1677 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1679 } else { #pkgnum tax or one-shot line item
1681 if ( $cust_bill_pkg->setup != 0 ) {
1683 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1685 if ( $cust_bill_pkg->recur != 0 ) {
1686 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1687 . time2str("%x", $cust_bill_pkg->edate). ")",
1688 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1696 push @buf,['','-----------'];
1697 push @buf,[ ( $conf->exists('disable_previous_balance')
1699 : 'Total New Charges'),
1700 $money_char. sprintf("%10.2f",$self->charged) ];
1703 unless ($conf->exists('disable_previous_balance')) {
1704 push @buf,['','-----------'];
1705 push @buf,['Total Charges',
1706 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1710 foreach ( $self->cust_credited ) {
1712 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1714 my $reason = substr($_->cust_credit->reason,0,32);
1715 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1716 $reason = " ($reason) " if $reason;
1718 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1720 $money_char. sprintf("%10.2f",$_->amount)
1723 #foreach ( @cr_cust_credit ) {
1725 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1726 # $money_char. sprintf("%10.2f",$_->credited)
1730 #get & print payments
1731 foreach ( $self->cust_bill_pay ) {
1733 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1736 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1737 $money_char. sprintf("%10.2f",$_->amount )
1742 my $balance_due_msg = $self->balance_due_msg;
1744 push @buf,['','-----------'];
1745 push @buf,[$balance_due_msg, $money_char.
1746 sprintf("%10.2f", $balance_due ) ];
1749 #create the template
1750 $template ||= $self->_agent_template;
1751 my $templatefile = 'invoice_template';
1752 $templatefile .= "_$template" if length($template);
1753 my @invoice_template = $conf->config($templatefile)
1754 or die "cannot load config file $templatefile";
1757 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1758 /invoice_lines\((\d*)\)/;
1759 $invoice_lines += $1 || scalar(@buf);
1762 die "no invoice_lines() functions in template?" unless $wasfunc;
1763 my $invoice_template = new Text::Template (
1765 SOURCE => [ map "$_\n", @invoice_template ],
1766 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1767 $invoice_template->compile()
1768 or die "can't compile template: $Text::Template::ERROR";
1770 #setup template variables
1771 package FS::cust_bill::_template; #!
1772 use vars qw( $custnum $invnum $date $agent @address $overdue
1773 $page $total_pages @buf );
1775 $custnum = $self->custnum;
1776 $invnum = $self->invnum;
1777 $date = $self->_date;
1778 $agent = $self->cust_main->agent->agent;
1781 if ( $FS::cust_bill::invoice_lines ) {
1783 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1785 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1790 #format address (variable for the template)
1792 @address = ( '', '', '', '', '', '' );
1793 package FS::cust_bill; #!
1794 $FS::cust_bill::_template::address[$l++] =
1795 $cust_main->payname.
1796 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1797 ? " (P.O. #". $cust_main->payinfo. ")"
1801 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1802 if $cust_main->company;
1803 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1804 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1805 if $cust_main->address2;
1806 $FS::cust_bill::_template::address[$l++] =
1807 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1809 my $countrydefault = $conf->config('countrydefault') || 'US';
1810 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1811 unless $cust_main->country eq $countrydefault;
1813 # #overdue? (variable for the template)
1814 # $FS::cust_bill::_template::overdue = (
1816 # && $today > $self->_date
1817 ## && $self->printed > 1
1818 # && $self->printed > 0
1821 #and subroutine for the template
1822 sub FS::cust_bill::_template::invoice_lines {
1823 my $lines = shift || scalar(@buf);
1825 scalar(@buf) ? shift @buf : [ '', '' ];
1831 $FS::cust_bill::_template::page = 1;
1835 push @collect, split("\n",
1836 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1838 $FS::cust_bill::_template::page++;
1841 map "$_\n", @collect;
1845 =item print_latex [ TIME [ , TEMPLATE ] ]
1847 Internal method - returns a filename of a filled-in LaTeX template for this
1848 invoice (Note: add ".tex" to get the actual filename).
1850 See print_ps and print_pdf for methods that return PostScript and PDF output.
1852 TIME an optional value used to control the printing of overdue messages. The
1853 default is now. It isn't the date of the invoice; that's the `_date' field.
1854 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1855 L<Time::Local> and L<Date::Parse> for conversion functions.
1859 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1862 my( $self, $today, $template ) = @_;
1864 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1867 my $cust_main = $self->cust_main;
1868 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1869 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1871 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1872 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1873 #my $balance_due = $self->owed + $pr_total - $cr_total;
1874 my $balance_due = $self->owed + $pr_total;
1876 #create the template
1877 $template ||= $self->_agent_template;
1878 my $templatefile = 'invoice_latex';
1879 my $suffix = length($template) ? "_$template" : '';
1880 $templatefile .= $suffix;
1881 my @invoice_template = map "$_\n", $conf->config($templatefile)
1882 or die "cannot load config file $templatefile";
1884 my($format, $text_template);
1885 if ( grep { /^%%Detail/ } @invoice_template ) {
1886 #change this to a die when the old code is removed
1887 warn "old-style invoice template $templatefile; ".
1888 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1891 $format = 'Text::Template';
1892 $text_template = new Text::Template(
1894 SOURCE => \@invoice_template,
1895 DELIMITERS => [ '[@--', '--@]' ],
1898 $text_template->compile()
1899 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1903 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1904 $returnaddress = join("\n",
1905 $conf->config_orbase('invoice_latexreturnaddress', $template)
1908 $returnaddress = '~';
1911 my %invoice_data = (
1912 'custnum' => $self->custnum,
1913 'invnum' => $self->invnum,
1914 'date' => time2str('%b %o, %Y', $self->_date),
1915 'today' => time2str('%b %o, %Y', $today),
1916 'agent' => _latex_escape($cust_main->agent->agent),
1917 'agent_custid' => _latex_escape($cust_main->agent_custid),
1918 'payname' => _latex_escape($cust_main->payname),
1919 'company' => _latex_escape($cust_main->company),
1920 'address1' => _latex_escape($cust_main->address1),
1921 'address2' => _latex_escape($cust_main->address2),
1922 'city' => _latex_escape($cust_main->city),
1923 'state' => _latex_escape($cust_main->state),
1925 'zip' => _latex_escape($cust_main->zip),
1926 'fax' => _latex_escape($cust_main->fax),
1927 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1928 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1929 'returnaddress' => $returnaddress,
1931 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1932 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1933 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1934 'current_charges' => sprintf('%.2f', $self->charged ),
1935 'previous_balance' => sprintf("%.2f", $pr_total),
1936 'balance' => sprintf("%.2f", $balance_due),
1937 'duedate' => $self->balance_due_date,
1938 'ship_enable' => $conf->exists('invoice-ship_address'),
1939 'unitprices' => $conf->exists('invoice-unitprice'),
1942 my $countrydefault = $conf->config('countrydefault') || 'US';
1943 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1944 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1945 my $method = $prefix.$_;
1946 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1948 $invoice_data{'ship_country'} = ''
1949 if ( $invoice_data{'ship_country'} eq $countrydefault );
1951 if ( $cust_main->country eq $countrydefault ) {
1952 $invoice_data{'country'} = '';
1954 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1957 $invoice_data{'notes'} =
1959 # #do variable substitutions in notes
1960 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1961 $conf->config_orbase('invoice_latexnotes', $template)
1963 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1966 #do variable substitution in coupon
1967 foreach my $include (qw( coupon )) {
1969 my @inc_src = $conf->config_orbase("invoice_latex$include", $template);
1971 my $inc_tt = new Text::Template (
1973 SOURCE => [ map "$_\n", @inc_src ],
1974 DELIMITERS => [ '[@--', '--@]' ],
1975 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
1977 unless ( $inc_tt->compile() ) {
1978 my $error = "Can't compile $include template: $Text::Template::ERROR\n";
1979 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
1983 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
1985 $invoice_data{$include} =~ s/\n+$//
1988 $invoice_data{'footer'} =~ s/\n+$//;
1989 $invoice_data{'smallfooter'} =~ s/\n+$//;
1990 $invoice_data{'notes'} =~ s/\n+$//;
1992 $invoice_data{'po_line'} =
1993 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1994 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1998 if ( $format eq 'old' ) {
2001 my @total_item = ();
2002 while ( @invoice_template ) {
2003 my $line = shift @invoice_template;
2005 if ( $line =~ /^%%Detail\s*$/ ) {
2007 while ( ( my $line_item_line = shift @invoice_template )
2008 !~ /^%%EndDetail\s*$/ ) {
2009 push @line_item, $line_item_line;
2011 foreach my $line_item ( $self->_items ) { #( 'format'=>'latex' ) ) {
2012 #foreach my $line_item ( $self->_items_pkg ) {
2013 $invoice_data{'ref'} = $line_item->{'pkgnum'};
2014 $invoice_data{'description'} =
2015 _latex_escape($line_item->{'description'});
2016 if ( exists $line_item->{'ext_description'} ) {
2017 $invoice_data{'description'} .=
2018 "\\tabularnewline\n~~".
2019 join( "\\tabularnewline\n~~",
2020 map _latex_escape($_), @{$line_item->{'ext_description'}}
2023 $invoice_data{'amount'} = $line_item->{'amount'};
2024 $invoice_data{'unit_amount'} = $line_item->{'unit_amount'};
2025 $invoice_data{'quantity'} = $line_item->{'quantity'};
2026 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2028 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
2031 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2033 while ( ( my $total_item_line = shift @invoice_template )
2034 !~ /^%%EndTotalDetails\s*$/ ) {
2035 push @total_item, $total_item_line;
2038 my @total_fill = ();
2041 foreach my $tax ( $self->_items_tax ) {
2042 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
2043 $taxtotal += $tax->{'amount'};
2044 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
2046 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2051 $invoice_data{'total_item'} = 'Sub-total';
2052 $invoice_data{'total_amount'} =
2053 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2054 unshift @total_fill,
2055 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2059 $invoice_data{'total_item'} = '\textbf{Total}';
2060 $invoice_data{'total_amount'} =
2061 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2063 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2066 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2069 foreach my $credit ( $self->_items_credits ) {
2070 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2072 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2074 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2079 foreach my $payment ( $self->_items_payments ) {
2080 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2082 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2084 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2088 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2089 $invoice_data{'total_amount'} =
2090 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2092 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2095 push @filled_in, @total_fill;
2098 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2099 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2100 push @filled_in, $line;
2111 } elsif ( $format eq 'Text::Template' ) {
2113 my @detail_items = ();
2114 my @total_items = ();
2116 $invoice_data{'detail_items'} = \@detail_items;
2117 $invoice_data{'total_items'} = \@total_items;
2119 my %options = ( 'format' => 'latex', 'escape_function' => \&_latex_escape );
2120 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2122 ext_description => [],
2124 $detail->{'ref'} = $line_item->{'pkgnum'};
2125 $detail->{'quantity'} = 1;
2126 $detail->{'description'} = _latex_escape($line_item->{'description'});
2127 if ( exists $line_item->{'ext_description'} ) {
2128 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2130 $detail->{'amount'} = $line_item->{'amount'};
2131 $detail->{'unit_amount'} = $line_item->{'unit_amount'};
2132 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2134 push @detail_items, $detail;
2139 foreach my $tax ( $self->_items_tax ) {
2141 $total->{'total_item'} = _latex_escape($tax->{'description'});
2142 $taxtotal += $tax->{'amount'};
2143 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2144 push @total_items, $total;
2148 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2150 $total->{'total_item'} = 'Sub-total';
2151 $total->{'total_amount'} =
2152 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2153 unshift @total_items, $total;
2155 $invoice_data{'taxtotal'} = '0.00';
2160 $total->{'total_item'} = '\textbf{Total}';
2161 $total->{'total_amount'} =
2164 $self->charged + ( $conf->exists('disable_previous_balance')
2170 push @total_items, $total;
2173 unless ($conf->exists('disable_previous_balance')) {
2174 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2177 my $credittotal = 0;
2178 foreach my $credit ( $self->_items_credits ) {
2180 $total->{'total_item'} = _latex_escape($credit->{'description'});
2181 $credittotal += $credit->{'amount'};
2182 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2183 push @total_items, $total;
2185 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2188 my $paymenttotal = 0;
2189 foreach my $payment ( $self->_items_payments ) {
2191 $total->{'total_item'} = _latex_escape($payment->{'description'});
2192 $paymenttotal += $payment->{'amount'};
2193 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2194 push @total_items, $total;
2196 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2200 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2201 $total->{'total_amount'} =
2202 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2203 push @total_items, $total;
2208 die "guru meditation #54";
2211 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2212 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2216 ) or die "can't open temp file: $!\n";
2217 if ( $format eq 'old' ) {
2218 print $fh join('', @filled_in );
2219 } elsif ( $format eq 'Text::Template' ) {
2220 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2222 die "guru meditation #32";
2226 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2231 =item print_ps [ TIME [ , TEMPLATE ] ]
2233 Returns an postscript invoice, as a scalar.
2235 TIME an optional value used to control the printing of overdue messages. The
2236 default is now. It isn't the date of the invoice; that's the `_date' field.
2237 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2238 L<Time::Local> and L<Date::Parse> for conversion functions.
2245 my $file = $self->print_latex(@_);
2246 my $ps = generate_ps($file);
2251 =item print_pdf [ TIME [ , TEMPLATE ] ]
2253 Returns an PDF invoice, as a scalar.
2255 TIME an optional value used to control the printing of overdue messages. The
2256 default is now. It isn't the date of the invoice; that's the `_date' field.
2257 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2258 L<Time::Local> and L<Date::Parse> for conversion functions.
2265 my $file = $self->print_latex(@_);
2266 my $pdf = generate_pdf($file);
2271 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2273 Returns an HTML invoice, as a scalar.
2275 TIME an optional value used to control the printing of overdue messages. The
2276 default is now. It isn't the date of the invoice; that's the `_date' field.
2277 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2278 L<Time::Local> and L<Date::Parse> for conversion functions.
2280 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2281 when emailing the invoice as part of a multipart/related MIME email.
2285 #some falze laziness w/print_text and print_latex (and send_csv)
2287 my( $self, $today, $template, $cid ) = @_;
2290 my $cust_main = $self->cust_main;
2291 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2292 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2294 $template ||= $self->_agent_template;
2295 my $templatefile = 'invoice_html';
2296 my $suffix = length($template) ? "_$template" : '';
2297 $templatefile .= $suffix;
2298 my @html_template = map "$_\n", $conf->config($templatefile)
2299 or die "cannot load config file $templatefile";
2301 my $html_template = new Text::Template(
2303 SOURCE => \@html_template,
2304 DELIMITERS => [ '<%=', '%>' ],
2307 $html_template->compile()
2308 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2310 my %invoice_data = (
2311 'custnum' => $self->custnum,
2312 'invnum' => $self->invnum,
2313 'date' => time2str('%b %o, %Y', $self->_date),
2314 'today' => time2str('%b %o, %Y', $today),
2315 'agent' => encode_entities($cust_main->agent->agent),
2316 'agent_custid' => encode_entities($cust_main->agent_custid),
2317 'payname' => encode_entities($cust_main->payname),
2318 'company' => encode_entities($cust_main->company),
2319 'address1' => encode_entities($cust_main->address1),
2320 'address2' => encode_entities($cust_main->address2),
2321 'city' => encode_entities($cust_main->city),
2322 'state' => encode_entities($cust_main->state),
2323 'zip' => encode_entities($cust_main->zip),
2324 'fax' => encode_entities($cust_main->fax),
2325 'terms' => $conf->config('invoice_default_terms')
2326 || 'Payable upon receipt',
2328 'template' => $template,
2329 'ship_enable' => $conf->exists('invoice-ship_address'),
2330 'unitprices' => $conf->exists('invoice-unitprice'),
2331 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2334 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2335 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2336 my $method = $prefix.$_;
2337 $invoice_data{"ship_$_"} = encode_entities($cust_main->$method);
2341 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2342 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2344 $invoice_data{'returnaddress'} =
2345 join("\n", $conf->config_orbase('invoice_htmlreturnaddress', $template) );
2347 $invoice_data{'returnaddress'} =
2350 s/\\\\\*?\s*$/<BR>/;
2351 s/\\hyphenation\{[\w\s\-]+\}//;
2355 $conf->config_orbase( 'invoice_latexreturnaddress',
2361 my $countrydefault = $conf->config('countrydefault') || 'US';
2362 if ( $cust_main->country eq $countrydefault ) {
2363 $invoice_data{'country'} = '';
2365 $invoice_data{'country'} =
2366 encode_entities(code2country($cust_main->country));
2370 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2371 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2373 $invoice_data{'notes'} =
2374 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2376 $invoice_data{'notes'} =
2378 s/%%(.*)$/<!-- $1 -->/g;
2379 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2380 s/\\begin\{enumerate\}/<ol>/g;
2382 s/\\end\{enumerate\}/<\/ol>/g;
2383 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2390 $conf->config_orbase('invoice_latexnotes', $template)
2394 # #do variable substitutions in notes
2395 # $invoice_data{'notes'} =
2397 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2398 # $conf->config_orbase('invoice_latexnotes', $suffix)
2402 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2403 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2405 $invoice_data{'footer'} =
2406 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2408 $invoice_data{'footer'} =
2409 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2410 $conf->config_orbase('invoice_latexfooter', $template)
2414 $invoice_data{'po_line'} =
2415 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2416 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2419 my $money_char = $conf->config('money_char') || '$';
2421 my %options = ( 'format' => 'html', 'escape_function' => \&encode_entities );
2422 foreach my $line_item ( ($conf->exists('disable_previous_balance') ? qw() : $self->_items_previous(%options)), $self->_items_pkg(%options) ) {
2424 ext_description => [],
2426 $detail->{'ref'} = $line_item->{'pkgnum'};
2427 $detail->{'description'} = encode_entities($line_item->{'description'});
2428 if ( exists $line_item->{'ext_description'} ) {
2429 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2431 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2432 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2434 push @{$invoice_data{'detail_items'}}, $detail;
2439 foreach my $tax ( $self->_items_tax ) {
2441 $total->{'total_item'} = encode_entities($tax->{'description'});
2442 $taxtotal += $tax->{'amount'};
2443 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2444 push @{$invoice_data{'total_items'}}, $total;
2449 $total->{'total_item'} = 'Sub-total';
2450 $total->{'total_amount'} =
2451 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2452 unshift @{$invoice_data{'total_items'}}, $total;
2455 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2458 $total->{'total_item'} = '<b>Total</b>';
2459 $total->{'total_amount'} =
2462 $self->charged + ( $conf->exists('disable_previous_balance')
2468 push @{$invoice_data{'total_items'}}, $total;
2471 unless ($conf->exists('disable_previous_balance')) {
2472 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2475 foreach my $credit ( $self->_items_credits ) {
2477 $total->{'total_item'} = encode_entities($credit->{'description'});
2479 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2480 push @{$invoice_data{'total_items'}}, $total;
2484 foreach my $payment ( $self->_items_payments ) {
2486 $total->{'total_item'} = encode_entities($payment->{'description'});
2488 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2489 push @{$invoice_data{'total_items'}}, $total;
2494 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2495 $total->{'total_amount'} =
2496 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2497 push @{$invoice_data{'total_items'}}, $total;
2501 $html_template->fill_in( HASH => \%invoice_data);
2504 # quick subroutine for print_latex
2506 # There are ten characters that LaTeX treats as special characters, which
2507 # means that they do not simply typeset themselves:
2508 # # $ % & ~ _ ^ \ { }
2510 # TeX ignores blanks following an escaped character; if you want a blank (as
2511 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2515 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2516 $value =~ s/([<>])/\$$1\$/g;
2520 #utility methods for print_*
2522 sub balance_due_msg {
2524 my $msg = 'Balance Due';
2525 return $msg unless $conf->exists('invoice_default_terms');
2526 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2527 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2528 } elsif ( $conf->config('invoice_default_terms') ) {
2529 $msg .= ' - '. $conf->config('invoice_default_terms');
2534 sub balance_due_date {
2537 if ( $conf->exists('invoice_default_terms')
2538 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2539 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2544 =item invnum_date_pretty
2546 Returns a string with the invoice number and date, for example:
2547 "Invoice #54 (3/20/2008)"
2551 sub invnum_date_pretty {
2553 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2558 Returns a string with the date, for example: "3/20/2008"
2564 time2str('%x', $self->_date);
2570 #my @display = scalar(@_)
2572 # : qw( _items_previous _items_pkg );
2573 # #: qw( _items_pkg );
2574 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2575 my @display = qw( _items_previous _items_pkg );
2578 foreach my $display ( @display ) {
2579 push @b, $self->$display(@_);
2584 sub _items_previous {
2586 my $cust_main = $self->cust_main;
2587 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2589 foreach ( @pr_cust_bill ) {
2591 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2592 ' ('. time2str('%x',$_->_date). ')',
2593 #'pkgpart' => 'N/A',
2595 'amount' => sprintf("%.2f", $_->owed),
2601 # 'description' => 'Previous Balance',
2602 # #'pkgpart' => 'N/A',
2603 # 'pkgnum' => 'N/A',
2604 # 'amount' => sprintf("%10.2f", $pr_total ),
2605 # 'ext_description' => [ map {
2606 # "Invoice ". $_->invnum.
2607 # " (". time2str("%x",$_->_date). ") ".
2608 # sprintf("%10.2f", $_->owed)
2609 # } @pr_cust_bill ],
2616 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2617 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2622 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2623 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2626 sub _items_cust_bill_pkg {
2628 my $cust_bill_pkg = shift;
2631 my $format = $opt{format} || '';
2632 my $escape_function = $opt{escape_function} || sub { shift };
2635 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2637 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2639 my $desc = $cust_bill_pkg->desc;
2640 $desc = substr($desc, 0, 50). '...'
2641 if $format eq 'latex' && length($desc) > 50;
2643 my %details_opt = ( 'format' => $format,
2644 'escape_function' => $escape_function,
2647 if ( $cust_bill_pkg->pkgnum > 0 ) {
2649 if ( $cust_bill_pkg->setup != 0 ) {
2651 my $description = $desc;
2652 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2655 push @d, map &{$escape_function}($_),
2656 $cust_pkg->h_labels_short($self->_date)
2657 unless $cust_pkg->part_pkg->hide_svc_detail;
2659 push @d, $cust_bill_pkg->details(%details_opt)
2660 if $cust_bill_pkg->recur == 0;
2663 description => $description,
2664 #pkgpart => $part_pkg->pkgpart,
2665 pkgnum => $cust_bill_pkg->pkgnum,
2666 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2667 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2668 quantity => $cust_bill_pkg->quantity,
2669 ext_description => \@d,
2673 if ( $cust_bill_pkg->recur != 0 ) {
2675 my $description = $desc;
2676 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2677 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2678 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2683 #at least until cust_bill_pkg has "past" ranges in addition to
2684 #the "future" sdate/edate ones... see #3032
2685 push @d, map &{$escape_function}($_),
2686 $cust_pkg->h_labels_short($self->_date)
2687 #$cust_bill_pkg->edate,
2688 #$cust_bill_pkg->sdate),
2689 unless $cust_pkg->part_pkg->hide_svc_detail
2690 || $cust_bill_pkg->itemdesc;
2692 push @d, $cust_bill_pkg->details(%details_opt);
2695 description => $description,
2696 #pkgpart => $part_pkg->pkgpart,
2697 pkgnum => $cust_bill_pkg->pkgnum,
2698 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2699 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2700 quantity => $cust_bill_pkg->quantity,
2701 ext_description => \@d,
2706 } else { #pkgnum tax or one-shot line item (??)
2708 if ( $cust_bill_pkg->setup != 0 ) {
2710 'description' => $desc,
2711 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2714 if ( $cust_bill_pkg->recur != 0 ) {
2716 'description' => "$desc (".
2717 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2718 time2str("%x", $cust_bill_pkg->edate). ')',
2719 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2731 sub _items_credits {
2736 foreach ( $self->cust_credited ) {
2738 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2740 my $reason = $_->cust_credit->reason;
2741 #my $reason = substr($_->cust_credit->reason,0,32);
2742 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2743 $reason = " ($reason) " if $reason;
2745 #'description' => 'Credit ref\#'. $_->crednum.
2746 # " (". time2str("%x",$_->cust_credit->_date) .")".
2748 'description' => 'Credit applied '.
2749 time2str("%x",$_->cust_credit->_date). $reason,
2750 'amount' => sprintf("%.2f",$_->amount),
2753 #foreach ( @cr_cust_credit ) {
2755 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2756 # $money_char. sprintf("%10.2f",$_->credited)
2764 sub _items_payments {
2768 #get & print payments
2769 foreach ( $self->cust_bill_pay ) {
2771 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2774 'description' => "Payment received ".
2775 time2str("%x",$_->cust_pay->_date ),
2776 'amount' => sprintf("%.2f", $_->amount )
2791 =item process_reprint
2795 sub process_reprint {
2796 process_re_X('print', @_);
2799 =item process_reemail
2803 sub process_reemail {
2804 process_re_X('email', @_);
2812 process_re_X('fax', @_);
2820 process_re_X('ftp', @_);
2827 sub process_respool {
2828 process_re_X('spool', @_);
2831 use Storable qw(thaw);
2835 my( $method, $job ) = ( shift, shift );
2836 warn "$me process_re_X $method for job $job\n" if $DEBUG;
2838 my $param = thaw(decode_base64(shift));
2839 warn Dumper($param) if $DEBUG;
2850 my($method, $job, %param ) = @_;
2852 warn "re_X $method for job $job with param:\n".
2853 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2856 #some false laziness w/search/cust_bill.html
2858 my $orderby = 'ORDER BY cust_bill._date';
2860 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
2862 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2864 my @cust_bill = qsearch( {
2865 #'select' => "cust_bill.*",
2866 'table' => 'cust_bill',
2867 'addl_from' => $addl_from,
2869 'extra_sql' => $extra_sql,
2870 'order_by' => $orderby,
2874 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2876 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2879 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2880 foreach my $cust_bill ( @cust_bill ) {
2881 $cust_bill->$method();
2883 if ( $job ) { #progressbar foo
2885 if ( time - $min_sec > $last ) {
2886 my $error = $job->update_statustext(
2887 int( 100 * $num / scalar(@cust_bill) )
2889 die $error if $error;
2900 =head1 CLASS METHODS
2906 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
2912 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
2917 Returns an SQL fragment to retreive the net amount (charged minus credited).
2923 'charged - '. $class->credited_sql;
2928 Returns an SQL fragment to retreive the amount paid against this invoice.
2934 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2935 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
2940 Returns an SQL fragment to retreive the amount credited against this invoice.
2946 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2947 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
2950 =item search_sql HASHREF
2952 Class method which returns an SQL WHERE fragment to search for parameters
2953 specified in HASHREF. Valid parameters are
2959 Epoch date (UNIX timestamp) setting a lower bound for _date values
2963 Epoch date (UNIX timestamp) setting an upper bound for _date values
2977 =item newest_percust
2981 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
2986 my($class, $param) = @_;
2988 warn "$me search_sql called with params: \n".
2989 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
2994 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
2995 push @search, "cust_bill._date >= $1";
2997 if ( $param->{'end'} =~ /^(\d+)$/ ) {
2998 push @search, "cust_bill._date < $1";
3000 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3001 push @search, "cust_bill.invnum >= $1";
3003 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3004 push @search, "cust_bill.invnum <= $1";
3006 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3007 push @search, "cust_main.agentnum = $1";
3010 push @search, '0 != '. FS::cust_bill->owed_sql
3011 if $param->{'open'};
3013 push @search, '0 != '. FS::cust_bill->net_sql
3016 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3017 if $param->{'days'};
3019 if ( $param->{'newest_percust'} ) {
3021 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3022 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3024 my @newest_where = map { my $x = $_;
3025 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3028 grep ! /^cust_main./, @search;
3029 my $newest_where = scalar(@newest_where)
3030 ? ' AND '. join(' AND ', @newest_where)
3034 push @search, "cust_bill._date = (
3035 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3036 WHERE newest_cust_bill.custnum = cust_bill.custnum
3042 my $curuser = $FS::CurrentUser::CurrentUser;
3043 if ( $curuser->username eq 'fs_queue'
3044 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3046 my $newuser = qsearchs('access_user', {
3047 'username' => $username,
3051 $curuser = $newuser;
3053 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3057 push @search, $curuser->agentnums_sql;
3059 join(' AND ', @search );
3071 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3072 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base