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);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main_Mixin;
20 use FS::cust_bill_pkg;
24 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 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.
229 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
234 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
241 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
243 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
246 =item open_cust_bill_pkg
248 Returns the open line items for this invoice.
250 Note that cust_bill_pkg with both setup and recur fees are returned as two
251 separate line items, each with only one fee.
255 # modeled after cust_main::open_cust_bill
256 sub open_cust_bill_pkg {
259 # grep { $_->owed > 0 } $self->cust_bill_pkg
261 my %other = ( 'recur' => 'setup',
262 'setup' => 'recur', );
264 foreach my $field ( qw( recur setup )) {
265 push @open, map { $_->set( $other{$field}, 0 ); $_; }
266 grep { $_->owed($field) > 0 }
267 $self->cust_bill_pkg;
273 =item cust_bill_event
275 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
280 sub cust_bill_event {
282 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
288 Returns the customer (see L<FS::cust_main>) for this invoice.
294 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
297 =item cust_suspend_if_balance_over AMOUNT
299 Suspends the customer associated with this invoice if the total amount owed on
300 this invoice and all older invoices is greater than the specified amount.
302 Returns a list: an empty list on success or a list of errors.
306 sub cust_suspend_if_balance_over {
307 my( $self, $amount ) = ( shift, shift );
308 my $cust_main = $self->cust_main;
309 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
312 $cust_main->suspend(@_);
318 Depreciated. See the cust_credited method.
320 #Returns a list consisting of the total previous credited (see
321 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
322 #outstanding credits (FS::cust_credit objects).
328 croak "FS::cust_bill->cust_credit depreciated; see ".
329 "FS::cust_bill->cust_credit_bill";
332 #my @cust_credit = sort { $a->_date <=> $b->_date }
333 # grep { $_->credited != 0 && $_->_date < $self->_date }
334 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
336 #foreach (@cust_credit) { $total += $_->credited; }
337 #$total, @cust_credit;
342 Depreciated. See the cust_bill_pay method.
344 #Returns all payments (see L<FS::cust_pay>) for this invoice.
350 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
352 #sort { $a->_date <=> $b->_date }
353 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
359 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
365 sort { $a->_date <=> $b->_date }
366 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
371 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
377 sort { $a->_date <=> $b->_date }
378 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
384 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
391 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
393 foreach (@taxlines) { $total += $_->setup; }
399 Returns the amount owed (still outstanding) on this invoice, which is charged
400 minus all payment applications (see L<FS::cust_bill_pay>) and credit
401 applications (see L<FS::cust_credit_bill>).
407 my $balance = $self->charged;
408 $balance -= $_->amount foreach ( $self->cust_bill_pay );
409 $balance -= $_->amount foreach ( $self->cust_credited );
410 $balance = sprintf( "%.2f", $balance);
411 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
415 =item apply_payments_and_credits
419 sub apply_payments_and_credits {
422 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
423 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
425 while ( $self->owed > 0 and ( @payments || @credits ) ) {
428 if ( @payments && @credits ) {
430 #decide which goes first by weight of top (unapplied) line item
432 my @open_lineitems = $self->open_cust_bill_pkg;
435 max( map { $_->cust_pkg->part_pkg->pay_weight || 0 }
438 my $max_credit_weight =
439 max( map { $_->cust_pkg->part_pkg->credit_weight || 0 }
443 #if both are the same... payments first? it has to be something
444 if ( $max_pay_weight >= $max_credit_weight ) {
450 } elsif ( @payments ) {
452 } elsif ( @credits ) {
455 die "guru meditation #12 and 35";
458 if ( $app eq 'pay' ) {
460 my $payment = shift @payments;
462 $app = new FS::cust_bill_pay {
463 'paynum' => $payment->paynum,
464 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
467 } elsif ( $app eq 'credit' ) {
469 my $credit = shift @credits;
471 $app = new FS::cust_credit_bill {
472 'crednum' => $credit->crednum,
473 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
477 die "guru meditation #12 and 35";
480 $app->invnum( $self->invnum );
482 my $error = $app->insert;
483 die $error if $error;
489 =item generate_email PARAMHASH
491 PARAMHASH can contain the following:
495 =item from => sender address, required
497 =item tempate => alternate template name, optional
499 =item print_text => text attachment arrayref, optional
501 =item subject => email subject, optional
505 Returns an argument list to be passed to L<FS::Misc::send_email>.
516 my $me = '[FS::cust_bill::generate_email]';
519 'from' => $args{'from'},
520 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
523 if (ref($args{'to'} eq 'ARRAY')) {
524 $return{'to'} = $args{'to'};
526 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
527 $self->cust_main->invoicing_list
531 if ( $conf->exists('invoice_html') ) {
533 warn "$me creating HTML/text multipart message"
536 $return{'nobody'} = 1;
538 my $alternative = build MIME::Entity
539 'Type' => 'multipart/alternative',
540 'Encoding' => '7bit',
541 'Disposition' => 'inline'
545 if ( $conf->exists('invoice_email_pdf')
546 and scalar($conf->config('invoice_email_pdf_note')) ) {
548 warn "$me using 'invoice_email_pdf_note' in multipart message"
550 $data = [ map { $_ . "\n" }
551 $conf->config('invoice_email_pdf_note')
556 warn "$me not using 'invoice_email_pdf_note' in multipart message"
558 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
559 $data = $args{'print_text'};
561 $data = [ $self->print_text('', $args{'template'}) ];
566 $alternative->attach(
567 'Type' => 'text/plain',
568 #'Encoding' => 'quoted-printable',
569 'Encoding' => '7bit',
571 'Disposition' => 'inline',
574 $args{'from'} =~ /\@([\w\.\-]+)/;
575 my $from = $1 || 'example.com';
576 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
578 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
580 if ( defined($args{'template'}) && length($args{'template'})
581 && -e "$path/logo_". $args{'template'}. ".png"
584 $file = "$path/logo_". $args{'template'}. ".png";
586 $file = "$path/logo.png";
589 my $image = build MIME::Entity
590 'Type' => 'image/png',
591 'Encoding' => 'base64',
593 'Filename' => 'logo.png',
594 'Content-ID' => "<$content_id>",
597 $alternative->attach(
598 'Type' => 'text/html',
599 'Encoding' => 'quoted-printable',
600 'Data' => [ '<html>',
603 ' '. encode_entities($return{'subject'}),
606 ' <body bgcolor="#e8e8e8">',
607 $self->print_html('', $args{'template'}, $content_id),
611 'Disposition' => 'inline',
612 #'Filename' => 'invoice.pdf',
615 if ( $conf->exists('invoice_email_pdf') ) {
620 # multipart/alternative
626 my $related = build MIME::Entity 'Type' => 'multipart/related',
627 'Encoding' => '7bit';
629 #false laziness w/Misc::send_email
630 $related->head->replace('Content-type',
632 '; boundary="'. $related->head->multipart_boundary. '"'.
633 '; type=multipart/alternative'
636 $related->add_part($alternative);
638 $related->add_part($image);
640 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
642 $return{'mimeparts'} = [ $related, $pdf ];
646 #no other attachment:
648 # multipart/alternative
653 $return{'content-type'} = 'multipart/related';
654 $return{'mimeparts'} = [ $alternative, $image ];
655 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
656 #$return{'disposition'} = 'inline';
662 if ( $conf->exists('invoice_email_pdf') ) {
663 warn "$me creating PDF attachment"
666 #mime parts arguments a la MIME::Entity->build().
667 $return{'mimeparts'} = [
668 { $self->mimebuild_pdf('', $args{'template'}) }
672 if ( $conf->exists('invoice_email_pdf')
673 and scalar($conf->config('invoice_email_pdf_note')) ) {
675 warn "$me using 'invoice_email_pdf_note'"
677 $return{'body'} = [ map { $_ . "\n" }
678 $conf->config('invoice_email_pdf_note')
683 warn "$me not using 'invoice_email_pdf_note'"
685 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
686 $return{'body'} = $args{'print_text'};
688 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
701 Returns a list suitable for passing to MIME::Entity->build(), representing
702 this invoice as PDF attachment.
709 'Type' => 'application/pdf',
710 'Encoding' => 'base64',
711 'Data' => [ $self->print_pdf(@_) ],
712 'Disposition' => 'attachment',
713 'Filename' => 'invoice.pdf',
717 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
719 Sends this invoice to the destinations configured for this customer: sends
720 email, prints and/or faxes. See L<FS::cust_main_invoice>.
722 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
724 AGENTNUM, if specified, means that this invoice will only be sent for customers
725 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
726 single agent) or an arrayref of agentnums.
728 INVOICE_FROM, if specified, overrides the default email invoice From: address.
735 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
736 or die "invalid invoice number: " . $opt{invnum};
738 my $error = $self->send($opt{template}, $opt{agentnum}, $opt{invoice_from});
740 die $error if $error;
745 my $template = scalar(@_) ? shift : '';
746 if ( scalar(@_) && $_[0] ) {
747 my $agentnums = ref($_[0]) ? shift : [ shift ];
748 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
754 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
756 my @invoicing_list = $self->cust_main->invoicing_list;
758 $self->email($template, $invoice_from)
759 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
761 $self->print($template)
762 if grep { $_ eq 'POST' } @invoicing_list; #postal
764 $self->fax($template)
765 if grep { $_ eq 'FAX' } @invoicing_list; #fax
771 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
775 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
777 INVOICE_FROM, if specified, overrides the default email invoice From: address.
783 my $template = scalar(@_) ? shift : '';
787 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
789 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
790 $self->cust_main->invoicing_list;
792 #better to notify this person than silence
793 @invoicing_list = ($invoice_from) unless @invoicing_list;
795 my $error = send_email(
796 $self->generate_email(
797 'from' => $invoice_from,
798 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
799 'template' => $template,
802 die "can't email invoice: $error\n" if $error;
803 #die "$error\n" if $error;
807 =item lpr_data [ TEMPLATENAME ]
809 Returns the postscript or plaintext for this invoice as an arrayref.
811 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
816 my( $self, $template) = @_;
817 $conf->exists('invoice_latex')
818 ? [ $self->print_ps('', $template) ]
819 : [ $self->print_text('', $template) ];
822 =item print [ TEMPLATENAME ]
826 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
832 my $template = scalar(@_) ? shift : '';
834 my $lpr = $conf->config('lpr');
837 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
839 $outerr = ": $outerr" if length($outerr);
840 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
845 =item fax [ TEMPLATENAME ]
849 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
855 my $template = scalar(@_) ? shift : '';
857 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
858 unless $conf->exists('invoice_latex');
860 my $dialstring = $self->cust_main->getfield('fax');
863 my $error = send_fax( 'docdata' => $self->lpr_data($template),
864 'dialstring' => $dialstring,
866 die $error if $error;
870 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
872 Like B<send>, but only sends the invoice if it is the newest open invoice for
882 grep { $_->owed > 0 }
883 qsearch('cust_bill', {
884 'custnum' => $self->custnum,
885 #'_date' => { op=>'>', value=>$self->_date },
886 'invnum' => { op=>'>', value=>$self->invnum },
893 =item send_csv OPTION => VALUE, ...
895 Sends invoice as a CSV data-file to a remote host with the specified protocol.
899 protocol - currently only "ftp"
905 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
906 and YYMMDDHHMMSS is a timestamp.
908 See L</print_csv> for a description of the output format.
913 my($self, %opt) = @_;
917 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
918 mkdir $spooldir, 0700 unless -d $spooldir;
920 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
921 my $file = "$spooldir/$tracctnum.csv";
923 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
925 open(CSV, ">$file") or die "can't open $file: $!";
933 if ( $opt{protocol} eq 'ftp' ) {
934 eval "use Net::FTP;";
936 $net = Net::FTP->new($opt{server}) or die @$;
938 die "unknown protocol: $opt{protocol}";
941 $net->login( $opt{username}, $opt{password} )
942 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
944 $net->binary or die "can't set binary mode";
946 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
948 $net->put($file) or die "can't put $file: $!";
958 Spools CSV invoice data.
964 =item format - 'default' or 'billco'
966 =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>).
968 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
970 =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.
977 my($self, %opt) = @_;
979 my $cust_main = $self->cust_main;
981 if ( $opt{'dest'} ) {
982 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
983 $cust_main->invoicing_list;
984 return 'N/A' unless $invoicing_list{$opt{'dest'}}
985 || ! keys %invoicing_list;
988 if ( $opt{'balanceover'} ) {
990 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
993 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
994 mkdir $spooldir, 0700 unless -d $spooldir;
996 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1000 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1001 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1004 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1006 open(CSV, ">>$file") or die "can't open $file: $!";
1007 flock(CSV, LOCK_EX);
1012 if ( lc($opt{'format'}) eq 'billco' ) {
1014 flock(CSV, LOCK_UN);
1019 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1022 open(CSV,">>$file") or die "can't open $file: $!";
1023 flock(CSV, LOCK_EX);
1029 flock(CSV, LOCK_UN);
1036 =item print_csv OPTION => VALUE, ...
1038 Returns CSV data for this invoice.
1042 format - 'default' or 'billco'
1044 Returns a list consisting of two scalars. The first is a single line of CSV
1045 header information for this invoice. The second is one or more lines of CSV
1046 detail information for this invoice.
1048 If I<format> is not specified or "default", the fields of the CSV file are as
1051 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1055 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1057 B<record_type> is C<cust_bill> for the initial header line only. The
1058 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1059 fields are filled in.
1061 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1062 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1065 =item invnum - invoice number
1067 =item custnum - customer number
1069 =item _date - invoice date
1071 =item charged - total invoice amount
1073 =item first - customer first name
1075 =item last - customer first name
1077 =item company - company name
1079 =item address1 - address line 1
1081 =item address2 - address line 1
1091 =item pkg - line item description
1093 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1095 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1097 =item sdate - start date for recurring fee
1099 =item edate - end date for recurring fee
1103 If I<format> is "billco", the fields of the header CSV file are as follows:
1105 +-------------------------------------------------------------------+
1106 | FORMAT HEADER FILE |
1107 |-------------------------------------------------------------------|
1108 | Field | Description | Name | Type | Width |
1109 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1110 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1111 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1112 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1113 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1114 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1115 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1116 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1117 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1118 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1119 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1120 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1121 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1122 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1123 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1124 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1125 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1126 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1127 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1128 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1129 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1130 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1131 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1132 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1133 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1134 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1135 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1136 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1137 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1138 +-------+-------------------------------+------------+------+-------+
1140 If I<format> is "billco", the fields of the detail CSV file are as follows:
1142 FORMAT FOR DETAIL FILE
1144 Field | Description | Name | Type | Width
1145 1 | N/A-Leave Empty | RC | CHAR | 2
1146 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1147 3 | Account Number | TRACCTNUM | CHAR | 15
1148 4 | Invoice Number | TRINVOICE | CHAR | 15
1149 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1150 6 | Transaction Detail | DETAILS | CHAR | 100
1151 7 | Amount | AMT | NUM* | 9
1152 8 | Line Format Control** | LNCTRL | CHAR | 2
1153 9 | Grouping Code | GROUP | CHAR | 2
1154 10 | User Defined | ACCT CODE | CHAR | 15
1159 my($self, %opt) = @_;
1161 eval "use Text::CSV_XS";
1164 my $cust_main = $self->cust_main;
1166 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1168 if ( lc($opt{'format'}) eq 'billco' ) {
1171 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1174 if ( $conf->exists('invoice_default_terms')
1175 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1176 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1179 my( $previous_balance, @unused ) = $self->previous; #previous balance
1181 my $pmt_cr_applied = 0;
1182 $pmt_cr_applied += $_->{'amount'}
1183 foreach ( $self->_items_payments, $self->_items_credits ) ;
1185 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1188 '', # 1 | N/A-Leave Empty CHAR 2
1189 '', # 2 | N/A-Leave Empty CHAR 15
1190 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1191 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1192 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1193 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1194 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1195 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1196 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1197 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1198 '', # 10 | Ancillary Billing Information CHAR 30
1199 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1200 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1203 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1206 $duedate, # 14 | Bill Due Date CHAR 10
1208 $previous_balance, # 15 | Previous Balance NUM* 9
1209 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1210 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1211 $totaldue, # 18 | Total Amt Due NUM* 9
1212 $totaldue, # 19 | Total Amt Due NUM* 9
1213 '', # 20 | 30 Day Aging NUM* 9
1214 '', # 21 | 60 Day Aging NUM* 9
1215 '', # 22 | 90 Day Aging NUM* 9
1216 'N', # 23 | Y/N CHAR 1
1217 '', # 24 | Remittance automation CHAR 100
1218 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1219 $self->custnum, # 26 | Customer Reference Number CHAR 15
1220 '0', # 27 | Federal Tax*** NUM* 9
1221 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1222 '0', # 29 | Other Taxes & Fees*** NUM* 9
1231 time2str("%x", $self->_date),
1232 sprintf("%.2f", $self->charged),
1233 ( map { $cust_main->getfield($_) }
1234 qw( first last company address1 address2 city state zip country ) ),
1236 ) or die "can't create csv";
1239 my $header = $csv->string. "\n";
1242 if ( lc($opt{'format'}) eq 'billco' ) {
1245 foreach my $item ( $self->_items_pkg ) {
1248 '', # 1 | N/A-Leave Empty CHAR 2
1249 '', # 2 | N/A-Leave Empty CHAR 15
1250 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1251 $self->invnum, # 4 | Invoice Number CHAR 15
1252 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1253 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1254 $item->{'amount'}, # 7 | Amount NUM* 9
1255 '', # 8 | Line Format Control** CHAR 2
1256 '', # 9 | Grouping Code CHAR 2
1257 '', # 10 | User Defined CHAR 15
1260 $detail .= $csv->string. "\n";
1266 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1268 my($pkg, $setup, $recur, $sdate, $edate);
1269 if ( $cust_bill_pkg->pkgnum ) {
1271 ($pkg, $setup, $recur, $sdate, $edate) = (
1272 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1273 ( $cust_bill_pkg->setup != 0
1274 ? sprintf("%.2f", $cust_bill_pkg->setup )
1276 ( $cust_bill_pkg->recur != 0
1277 ? sprintf("%.2f", $cust_bill_pkg->recur )
1279 ( $cust_bill_pkg->sdate
1280 ? time2str("%x", $cust_bill_pkg->sdate)
1282 ($cust_bill_pkg->edate
1283 ?time2str("%x", $cust_bill_pkg->edate)
1287 } else { #pkgnum tax
1288 next unless $cust_bill_pkg->setup != 0;
1289 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1290 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1292 ($pkg, $setup, $recur, $sdate, $edate) =
1293 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1299 ( map { '' } (1..11) ),
1300 ($pkg, $setup, $recur, $sdate, $edate)
1301 ) or die "can't create csv";
1303 $detail .= $csv->string. "\n";
1309 ( $header, $detail );
1315 Pays this invoice with a compliemntary payment. If there is an error,
1316 returns the error, otherwise returns false.
1322 my $cust_pay = new FS::cust_pay ( {
1323 'invnum' => $self->invnum,
1324 'paid' => $self->owed,
1327 'payinfo' => $self->cust_main->payinfo,
1335 Attempts to pay this invoice with a credit card payment via a
1336 Business::OnlinePayment realtime gateway. See
1337 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1338 for supported processors.
1344 $self->realtime_bop( 'CC', @_ );
1349 Attempts to pay this invoice with an electronic check (ACH) payment via a
1350 Business::OnlinePayment realtime gateway. See
1351 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1352 for supported processors.
1358 $self->realtime_bop( 'ECHECK', @_ );
1363 Attempts to pay this invoice with phone bill (LEC) payment via a
1364 Business::OnlinePayment realtime gateway. See
1365 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1366 for supported processors.
1372 $self->realtime_bop( 'LEC', @_ );
1376 my( $self, $method ) = @_;
1378 my $cust_main = $self->cust_main;
1379 my $balance = $cust_main->balance;
1380 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1381 $amount = sprintf("%.2f", $amount);
1382 return "not run (balance $balance)" unless $amount > 0;
1384 my $description = 'Internet Services';
1385 if ( $conf->exists('business-onlinepayment-description') ) {
1386 my $dtempl = $conf->config('business-onlinepayment-description');
1388 my $agent_obj = $cust_main->agent
1389 or die "can't retreive agent for $cust_main (agentnum ".
1390 $cust_main->agentnum. ")";
1391 my $agent = $agent_obj->agent;
1392 my $pkgs = join(', ',
1393 map { $_->cust_pkg->part_pkg->pkg }
1394 grep { $_->pkgnum } $self->cust_bill_pkg
1396 $description = eval qq("$dtempl");
1399 $cust_main->realtime_bop($method, $amount,
1400 'description' => $description,
1401 'invnum' => $self->invnum,
1406 =item batch_card OPTION => VALUE...
1408 Adds a payment for this invoice to the pending credit card batch (see
1409 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1410 runs the payment using a realtime gateway.
1415 my ($self, %options) = @_;
1416 my $cust_main = $self->cust_main;
1418 my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1419 return '' unless $amount > 0;
1421 if ($options{'realtime'}) {
1422 return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1428 my $oldAutoCommit = $FS::UID::AutoCommit;
1429 local $FS::UID::AutoCommit = 0;
1432 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1433 or return "Cannot lock pay_batch: " . $dbh->errstr;
1437 'payby' => FS::payby->payby2payment($cust_main->payby),
1440 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1442 unless ( $pay_batch ) {
1443 $pay_batch = new FS::pay_batch \%pay_batch;
1444 my $error = $pay_batch->insert;
1446 $dbh->rollback if $oldAutoCommit;
1447 die "error creating new batch: $error\n";
1451 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1452 'batchnum' => $pay_batch->batchnum,
1453 'custnum' => $cust_main->custnum,
1456 my $cust_pay_batch = new FS::cust_pay_batch ( {
1457 'batchnum' => $pay_batch->batchnum,
1458 'invnum' => $self->getfield('invnum'), # is there a better value?
1459 # this field should be
1461 # cust_bill_pay_batch now
1462 'custnum' => $cust_main->custnum,
1463 'last' => $cust_main->getfield('last'),
1464 'first' => $cust_main->getfield('first'),
1465 'address1' => $cust_main->address1,
1466 'address2' => $cust_main->address2,
1467 'city' => $cust_main->city,
1468 'state' => $cust_main->state,
1469 'zip' => $cust_main->zip,
1470 'country' => $cust_main->country,
1471 'payby' => $cust_main->payby,
1472 'payinfo' => $cust_main->payinfo,
1473 'exp' => $cust_main->paydate,
1474 'payname' => $cust_main->payname,
1475 'amount' => $amount, # consolidating
1478 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1479 if $old_cust_pay_batch;
1482 if ($old_cust_pay_batch) {
1483 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1485 $error = $cust_pay_batch->insert;
1489 $dbh->rollback if $oldAutoCommit;
1493 my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1494 foreach my $cust_bill ($cust_main->open_cust_bill) {
1495 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1496 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1497 'invnum' => $cust_bill->invnum,
1498 'paybatchnum' => $cust_pay_batch->paybatchnum,
1499 'amount' => $cust_bill->owed,
1502 if ($unapplied >= $cust_bill_pay_batch->amount){
1503 $unapplied -= $cust_bill_pay_batch->amount;
1506 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
1507 $cust_bill_pay_batch->amount - $unapplied ));
1510 $error = $cust_bill_pay_batch->insert;
1512 $dbh->rollback if $oldAutoCommit;
1517 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1521 sub _agent_template {
1523 $self->_agent_plandata('agent_templatename');
1526 sub _agent_invoice_from {
1528 $self->_agent_plandata('agent_invoice_from');
1531 sub _agent_plandata {
1532 my( $self, $option ) = @_;
1534 my $part_bill_event = qsearchs( 'part_bill_event',
1536 'payby' => $self->cust_main->payby,
1537 'plan' => 'send_agent',
1538 'plandata' => { 'op' => '~',
1539 'value' => "(^|\n)agentnum ".
1541 $self->cust_main->agentnum.
1547 'ORDER BY seconds LIMIT 1'
1550 return '' unless $part_bill_event;
1552 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1555 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1556 " plandata for $option";
1562 =item print_text [ TIME [ , TEMPLATE ] ]
1564 Returns an text invoice, as a list of lines.
1566 TIME an optional value used to control the printing of overdue messages. The
1567 default is now. It isn't the date of the invoice; that's the `_date' field.
1568 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1569 L<Time::Local> and L<Date::Parse> for conversion functions.
1573 #still some false laziness w/_items stuff (and send_csv)
1576 my( $self, $today, $template ) = @_;
1579 # my $invnum = $self->invnum;
1580 my $cust_main = $self->cust_main;
1581 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1582 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1584 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1585 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1586 #my $balance_due = $self->owed + $pr_total - $cr_total;
1587 my $balance_due = $self->owed + $pr_total;
1590 #my($description,$amount);
1594 foreach ( @pr_cust_bill ) {
1596 "Previous Balance, Invoice #". $_->invnum.
1597 " (". time2str("%x",$_->_date). ")",
1598 $money_char. sprintf("%10.2f",$_->owed)
1601 if (@pr_cust_bill) {
1602 push @buf,['','-----------'];
1603 push @buf,[ 'Total Previous Balance',
1604 $money_char. sprintf("%10.2f",$pr_total ) ];
1609 foreach my $cust_bill_pkg (
1610 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1611 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1614 my $desc = $cust_bill_pkg->desc;
1616 if ( $cust_bill_pkg->pkgnum > 0 ) {
1618 if ( $cust_bill_pkg->setup != 0 ) {
1619 my $description = $desc;
1620 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1621 push @buf, [ $description,
1622 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1624 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1625 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1628 if ( $cust_bill_pkg->recur != 0 ) {
1630 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1631 time2str("%x", $cust_bill_pkg->edate) . ")",
1632 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1635 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1636 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1637 $cust_bill_pkg->sdate );
1640 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1642 } else { #pkgnum tax or one-shot line item
1644 if ( $cust_bill_pkg->setup != 0 ) {
1646 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1648 if ( $cust_bill_pkg->recur != 0 ) {
1649 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1650 . time2str("%x", $cust_bill_pkg->edate). ")",
1651 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1659 push @buf,['','-----------'];
1660 push @buf,['Total New Charges',
1661 $money_char. sprintf("%10.2f",$self->charged) ];
1664 push @buf,['','-----------'];
1665 push @buf,['Total Charges',
1666 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1670 foreach ( $self->cust_credited ) {
1672 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1674 my $reason = substr($_->cust_credit->reason,0,32);
1675 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1676 $reason = " ($reason) " if $reason;
1678 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1680 $money_char. sprintf("%10.2f",$_->amount)
1683 #foreach ( @cr_cust_credit ) {
1685 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1686 # $money_char. sprintf("%10.2f",$_->credited)
1690 #get & print payments
1691 foreach ( $self->cust_bill_pay ) {
1693 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1696 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1697 $money_char. sprintf("%10.2f",$_->amount )
1702 my $balance_due_msg = $self->balance_due_msg;
1704 push @buf,['','-----------'];
1705 push @buf,[$balance_due_msg, $money_char.
1706 sprintf("%10.2f", $balance_due ) ];
1708 #create the template
1709 $template ||= $self->_agent_template;
1710 my $templatefile = 'invoice_template';
1711 $templatefile .= "_$template" if length($template);
1712 my @invoice_template = $conf->config($templatefile)
1713 or die "cannot load config file $templatefile";
1716 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1717 /invoice_lines\((\d*)\)/;
1718 $invoice_lines += $1 || scalar(@buf);
1721 die "no invoice_lines() functions in template?" unless $wasfunc;
1722 my $invoice_template = new Text::Template (
1724 SOURCE => [ map "$_\n", @invoice_template ],
1725 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1726 $invoice_template->compile()
1727 or die "can't compile template: $Text::Template::ERROR";
1729 #setup template variables
1730 package FS::cust_bill::_template; #!
1731 use vars qw( $custnum $invnum $date $agent @address $overdue
1732 $page $total_pages @buf );
1734 $custnum = $self->custnum;
1735 $invnum = $self->invnum;
1736 $date = $self->_date;
1737 $agent = $self->cust_main->agent->agent;
1740 if ( $FS::cust_bill::invoice_lines ) {
1742 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1744 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1749 #format address (variable for the template)
1751 @address = ( '', '', '', '', '', '' );
1752 package FS::cust_bill; #!
1753 $FS::cust_bill::_template::address[$l++] =
1754 $cust_main->payname.
1755 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1756 ? " (P.O. #". $cust_main->payinfo. ")"
1760 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1761 if $cust_main->company;
1762 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1763 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1764 if $cust_main->address2;
1765 $FS::cust_bill::_template::address[$l++] =
1766 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1768 my $countrydefault = $conf->config('countrydefault') || 'US';
1769 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1770 unless $cust_main->country eq $countrydefault;
1772 # #overdue? (variable for the template)
1773 # $FS::cust_bill::_template::overdue = (
1775 # && $today > $self->_date
1776 ## && $self->printed > 1
1777 # && $self->printed > 0
1780 #and subroutine for the template
1781 sub FS::cust_bill::_template::invoice_lines {
1782 my $lines = shift || scalar(@buf);
1784 scalar(@buf) ? shift @buf : [ '', '' ];
1790 $FS::cust_bill::_template::page = 1;
1794 push @collect, split("\n",
1795 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1797 $FS::cust_bill::_template::page++;
1800 map "$_\n", @collect;
1804 =item print_latex [ TIME [ , TEMPLATE ] ]
1806 Internal method - returns a filename of a filled-in LaTeX template for this
1807 invoice (Note: add ".tex" to get the actual filename).
1809 See print_ps and print_pdf for methods that return PostScript and PDF output.
1811 TIME an optional value used to control the printing of overdue messages. The
1812 default is now. It isn't the date of the invoice; that's the `_date' field.
1813 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1814 L<Time::Local> and L<Date::Parse> for conversion functions.
1818 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1821 my( $self, $today, $template ) = @_;
1823 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1826 my $cust_main = $self->cust_main;
1827 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1828 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1830 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1831 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1832 #my $balance_due = $self->owed + $pr_total - $cr_total;
1833 my $balance_due = $self->owed + $pr_total;
1835 #create the template
1836 $template ||= $self->_agent_template;
1837 my $templatefile = 'invoice_latex';
1838 my $suffix = length($template) ? "_$template" : '';
1839 $templatefile .= $suffix;
1840 my @invoice_template = map "$_\n", $conf->config($templatefile)
1841 or die "cannot load config file $templatefile";
1843 my($format, $text_template);
1844 if ( grep { /^%%Detail/ } @invoice_template ) {
1845 #change this to a die when the old code is removed
1846 warn "old-style invoice template $templatefile; ".
1847 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1850 $format = 'Text::Template';
1851 $text_template = new Text::Template(
1853 SOURCE => \@invoice_template,
1854 DELIMITERS => [ '[@--', '--@]' ],
1857 $text_template->compile()
1858 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1862 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1863 $returnaddress = join("\n",
1864 $conf->config_orbase('invoice_latexreturnaddress', $template)
1867 $returnaddress = '~';
1870 my %invoice_data = (
1871 'custnum' => $self->custnum,
1872 'invnum' => $self->invnum,
1873 'date' => time2str('%b %o, %Y', $self->_date),
1874 'today' => time2str('%b %o, %Y', $today),
1875 'agent' => _latex_escape($cust_main->agent->agent),
1876 'payname' => _latex_escape($cust_main->payname),
1877 'company' => _latex_escape($cust_main->company),
1878 'address1' => _latex_escape($cust_main->address1),
1879 'address2' => _latex_escape($cust_main->address2),
1880 'city' => _latex_escape($cust_main->city),
1881 'state' => _latex_escape($cust_main->state),
1882 'zip' => _latex_escape($cust_main->zip),
1883 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1884 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1885 'returnaddress' => $returnaddress,
1887 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1888 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1889 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1892 my $countrydefault = $conf->config('countrydefault') || 'US';
1893 if ( $cust_main->country eq $countrydefault ) {
1894 $invoice_data{'country'} = '';
1896 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1899 $invoice_data{'notes'} =
1901 # #do variable substitutions in notes
1902 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1903 $conf->config_orbase('invoice_latexnotes', $template)
1905 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1908 $invoice_data{'footer'} =~ s/\n+$//;
1909 $invoice_data{'smallfooter'} =~ s/\n+$//;
1910 $invoice_data{'notes'} =~ s/\n+$//;
1912 $invoice_data{'po_line'} =
1913 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1914 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1918 if ( $format eq 'old' ) {
1921 my @total_item = ();
1922 while ( @invoice_template ) {
1923 my $line = shift @invoice_template;
1925 if ( $line =~ /^%%Detail\s*$/ ) {
1927 while ( ( my $line_item_line = shift @invoice_template )
1928 !~ /^%%EndDetail\s*$/ ) {
1929 push @line_item, $line_item_line;
1931 foreach my $line_item ( $self->_items ) {
1932 #foreach my $line_item ( $self->_items_pkg ) {
1933 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1934 $invoice_data{'description'} =
1935 _latex_escape($line_item->{'description'});
1936 if ( exists $line_item->{'ext_description'} ) {
1937 $invoice_data{'description'} .=
1938 "\\tabularnewline\n~~".
1939 join( "\\tabularnewline\n~~",
1940 map _latex_escape($_), @{$line_item->{'ext_description'}}
1943 $invoice_data{'amount'} = $line_item->{'amount'};
1944 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1946 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1949 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1951 while ( ( my $total_item_line = shift @invoice_template )
1952 !~ /^%%EndTotalDetails\s*$/ ) {
1953 push @total_item, $total_item_line;
1956 my @total_fill = ();
1959 foreach my $tax ( $self->_items_tax ) {
1960 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1961 $taxtotal += $tax->{'amount'};
1962 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1964 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1969 $invoice_data{'total_item'} = 'Sub-total';
1970 $invoice_data{'total_amount'} =
1971 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1972 unshift @total_fill,
1973 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1977 $invoice_data{'total_item'} = '\textbf{Total}';
1978 $invoice_data{'total_amount'} =
1979 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1981 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1984 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1987 foreach my $credit ( $self->_items_credits ) {
1988 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1990 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1992 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1997 foreach my $payment ( $self->_items_payments ) {
1998 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2000 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2002 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2006 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2007 $invoice_data{'total_amount'} =
2008 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2010 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2013 push @filled_in, @total_fill;
2016 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2017 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2018 push @filled_in, $line;
2029 } elsif ( $format eq 'Text::Template' ) {
2031 my @detail_items = ();
2032 my @total_items = ();
2034 $invoice_data{'detail_items'} = \@detail_items;
2035 $invoice_data{'total_items'} = \@total_items;
2037 foreach my $line_item ( $self->_items ) {
2039 ext_description => [],
2041 $detail->{'ref'} = $line_item->{'pkgnum'};
2042 $detail->{'quantity'} = 1;
2043 $detail->{'description'} = _latex_escape($line_item->{'description'});
2044 if ( exists $line_item->{'ext_description'} ) {
2045 @{$detail->{'ext_description'}} = map {
2047 } @{$line_item->{'ext_description'}};
2049 $detail->{'amount'} = $line_item->{'amount'};
2050 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2052 push @detail_items, $detail;
2057 foreach my $tax ( $self->_items_tax ) {
2059 $total->{'total_item'} = _latex_escape($tax->{'description'});
2060 $taxtotal += $tax->{'amount'};
2061 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2062 push @total_items, $total;
2067 $total->{'total_item'} = 'Sub-total';
2068 $total->{'total_amount'} =
2069 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2070 unshift @total_items, $total;
2075 $total->{'total_item'} = '\textbf{Total}';
2076 $total->{'total_amount'} =
2077 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2078 push @total_items, $total;
2081 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2084 foreach my $credit ( $self->_items_credits ) {
2086 $total->{'total_item'} = _latex_escape($credit->{'description'});
2088 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2089 push @total_items, $total;
2093 foreach my $payment ( $self->_items_payments ) {
2095 $total->{'total_item'} = _latex_escape($payment->{'description'});
2097 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2098 push @total_items, $total;
2103 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2104 $total->{'total_amount'} =
2105 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2106 push @total_items, $total;
2110 die "guru meditation #54";
2113 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2114 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2118 ) or die "can't open temp file: $!\n";
2119 if ( $format eq 'old' ) {
2120 print $fh join('', @filled_in );
2121 } elsif ( $format eq 'Text::Template' ) {
2122 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2124 die "guru meditation #32";
2128 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2133 =item print_ps [ TIME [ , TEMPLATE ] ]
2135 Returns an postscript invoice, as a scalar.
2137 TIME an optional value used to control the printing of overdue messages. The
2138 default is now. It isn't the date of the invoice; that's the `_date' field.
2139 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2140 L<Time::Local> and L<Date::Parse> for conversion functions.
2147 my $file = $self->print_latex(@_);
2149 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2152 my $sfile = shell_quote $file;
2154 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2155 or die "pslatex $file.tex failed; see $file.log for details?\n";
2156 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2157 or die "pslatex $file.tex failed; see $file.log for details?\n";
2159 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2160 or die "dvips failed";
2162 open(POSTSCRIPT, "<$file.ps")
2163 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2165 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2168 while (<POSTSCRIPT>) {
2178 =item print_pdf [ TIME [ , TEMPLATE ] ]
2180 Returns an PDF invoice, as a scalar.
2182 TIME an optional value used to control the printing of overdue messages. The
2183 default is now. It isn't the date of the invoice; that's the `_date' field.
2184 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2185 L<Time::Local> and L<Date::Parse> for conversion functions.
2192 my $file = $self->print_latex(@_);
2194 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2197 #system('pdflatex', "$file.tex");
2198 #system('pdflatex', "$file.tex");
2199 #! LaTeX Error: Unknown graphics extension: .eps.
2201 my $sfile = shell_quote $file;
2203 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2204 or die "pslatex $file.tex failed; see $file.log for details?\n";
2205 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2206 or die "pslatex $file.tex failed; see $file.log for details?\n";
2208 #system('dvipdf', "$file.dvi", "$file.pdf" );
2210 "dvips -q -t letter -f $sfile.dvi ".
2211 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2214 or die "dvips | gs failed: $!";
2216 open(PDF, "<$file.pdf")
2217 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2219 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2232 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2234 Returns an HTML invoice, as a scalar.
2236 TIME an optional value used to control the printing of overdue messages. The
2237 default is now. It isn't the date of the invoice; that's the `_date' field.
2238 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2239 L<Time::Local> and L<Date::Parse> for conversion functions.
2241 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2242 when emailing the invoice as part of a multipart/related MIME email.
2246 #some falze laziness w/print_text and print_latex (and send_csv)
2248 my( $self, $today, $template, $cid ) = @_;
2251 my $cust_main = $self->cust_main;
2252 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2253 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2255 $template ||= $self->_agent_template;
2256 my $templatefile = 'invoice_html';
2257 my $suffix = length($template) ? "_$template" : '';
2258 $templatefile .= $suffix;
2259 my @html_template = map "$_\n", $conf->config($templatefile)
2260 or die "cannot load config file $templatefile";
2262 my $html_template = new Text::Template(
2264 SOURCE => \@html_template,
2265 DELIMITERS => [ '<%=', '%>' ],
2268 $html_template->compile()
2269 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2271 my %invoice_data = (
2272 'custnum' => $self->custnum,
2273 'invnum' => $self->invnum,
2274 'date' => time2str('%b %o, %Y', $self->_date),
2275 'today' => time2str('%b %o, %Y', $today),
2276 'agent' => encode_entities($cust_main->agent->agent),
2277 'payname' => encode_entities($cust_main->payname),
2278 'company' => encode_entities($cust_main->company),
2279 'address1' => encode_entities($cust_main->address1),
2280 'address2' => encode_entities($cust_main->address2),
2281 'city' => encode_entities($cust_main->city),
2282 'state' => encode_entities($cust_main->state),
2283 'zip' => encode_entities($cust_main->zip),
2284 'terms' => $conf->config('invoice_default_terms')
2285 || 'Payable upon receipt',
2287 'template' => $template,
2288 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2292 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2293 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2295 $invoice_data{'returnaddress'} =
2296 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2298 $invoice_data{'returnaddress'} =
2301 s/\\\\\*?\s*$/<BR>/;
2302 s/\\hyphenation\{[\w\s\-]+\}//;
2305 $conf->config_orbase( 'invoice_latexreturnaddress',
2311 my $countrydefault = $conf->config('countrydefault') || 'US';
2312 if ( $cust_main->country eq $countrydefault ) {
2313 $invoice_data{'country'} = '';
2315 $invoice_data{'country'} =
2316 encode_entities(code2country($cust_main->country));
2320 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2321 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2323 $invoice_data{'notes'} =
2324 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2326 $invoice_data{'notes'} =
2328 s/%%(.*)$/<!-- $1 -->/;
2329 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2330 s/\\begin\{enumerate\}/<ol>/;
2332 s/\\end\{enumerate\}/<\/ol>/;
2333 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2336 $conf->config_orbase('invoice_latexnotes', $template)
2340 # #do variable substitutions in notes
2341 # $invoice_data{'notes'} =
2343 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2344 # $conf->config_orbase('invoice_latexnotes', $suffix)
2348 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2349 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2351 $invoice_data{'footer'} =
2352 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2354 $invoice_data{'footer'} =
2355 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2356 $conf->config_orbase('invoice_latexfooter', $template)
2360 $invoice_data{'po_line'} =
2361 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2362 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2365 my $money_char = $conf->config('money_char') || '$';
2367 foreach my $line_item ( $self->_items ) {
2369 ext_description => [],
2371 $detail->{'ref'} = $line_item->{'pkgnum'};
2372 $detail->{'description'} = encode_entities($line_item->{'description'});
2373 if ( exists $line_item->{'ext_description'} ) {
2374 @{$detail->{'ext_description'}} = map {
2375 encode_entities($_);
2376 } @{$line_item->{'ext_description'}};
2378 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2379 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2381 push @{$invoice_data{'detail_items'}}, $detail;
2386 foreach my $tax ( $self->_items_tax ) {
2388 $total->{'total_item'} = encode_entities($tax->{'description'});
2389 $taxtotal += $tax->{'amount'};
2390 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2391 push @{$invoice_data{'total_items'}}, $total;
2396 $total->{'total_item'} = 'Sub-total';
2397 $total->{'total_amount'} =
2398 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2399 unshift @{$invoice_data{'total_items'}}, $total;
2402 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2405 $total->{'total_item'} = '<b>Total</b>';
2406 $total->{'total_amount'} =
2407 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2408 push @{$invoice_data{'total_items'}}, $total;
2411 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2414 foreach my $credit ( $self->_items_credits ) {
2416 $total->{'total_item'} = encode_entities($credit->{'description'});
2418 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2419 push @{$invoice_data{'total_items'}}, $total;
2423 foreach my $payment ( $self->_items_payments ) {
2425 $total->{'total_item'} = encode_entities($payment->{'description'});
2427 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2428 push @{$invoice_data{'total_items'}}, $total;
2433 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2434 $total->{'total_amount'} =
2435 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2436 push @{$invoice_data{'total_items'}}, $total;
2439 $html_template->fill_in( HASH => \%invoice_data);
2442 # quick subroutine for print_latex
2444 # There are ten characters that LaTeX treats as special characters, which
2445 # means that they do not simply typeset themselves:
2446 # # $ % & ~ _ ^ \ { }
2448 # TeX ignores blanks following an escaped character; if you want a blank (as
2449 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2453 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2454 $value =~ s/([<>])/\$$1\$/g;
2458 #utility methods for print_*
2460 sub balance_due_msg {
2462 my $msg = 'Balance Due';
2463 return $msg unless $conf->exists('invoice_default_terms');
2464 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2465 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2466 } elsif ( $conf->config('invoice_default_terms') ) {
2467 $msg .= ' - '. $conf->config('invoice_default_terms');
2474 my @display = scalar(@_)
2476 : qw( _items_previous _items_pkg );
2477 #: qw( _items_pkg );
2478 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2480 foreach my $display ( @display ) {
2481 push @b, $self->$display(@_);
2486 sub _items_previous {
2488 my $cust_main = $self->cust_main;
2489 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2491 foreach ( @pr_cust_bill ) {
2493 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2494 ' ('. time2str('%x',$_->_date). ')',
2495 #'pkgpart' => 'N/A',
2497 'amount' => sprintf("%.2f", $_->owed),
2503 # 'description' => 'Previous Balance',
2504 # #'pkgpart' => 'N/A',
2505 # 'pkgnum' => 'N/A',
2506 # 'amount' => sprintf("%10.2f", $pr_total ),
2507 # 'ext_description' => [ map {
2508 # "Invoice ". $_->invnum.
2509 # " (". time2str("%x",$_->_date). ") ".
2510 # sprintf("%10.2f", $_->owed)
2511 # } @pr_cust_bill ],
2518 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2519 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2524 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2525 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2528 sub _items_cust_bill_pkg {
2530 my $cust_bill_pkg = shift;
2533 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2535 my $desc = $cust_bill_pkg->desc;
2537 if ( $cust_bill_pkg->pkgnum > 0 ) {
2539 if ( $cust_bill_pkg->setup != 0 ) {
2540 my $description = $desc;
2541 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2542 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2543 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2545 description => $description,
2546 #pkgpart => $part_pkg->pkgpart,
2547 pkgnum => $cust_bill_pkg->pkgnum,
2548 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2549 ext_description => \@d,
2553 if ( $cust_bill_pkg->recur != 0 ) {
2555 description => "$desc (" .
2556 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2557 time2str('%x', $cust_bill_pkg->edate). ')',
2558 #pkgpart => $part_pkg->pkgpart,
2559 pkgnum => $cust_bill_pkg->pkgnum,
2560 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2562 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2563 $cust_bill_pkg->sdate),
2564 $cust_bill_pkg->details,
2569 } else { #pkgnum tax or one-shot line item (??)
2571 if ( $cust_bill_pkg->setup != 0 ) {
2573 'description' => $desc,
2574 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2577 if ( $cust_bill_pkg->recur != 0 ) {
2579 'description' => "$desc (".
2580 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2581 time2str("%x", $cust_bill_pkg->edate). ')',
2582 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2594 sub _items_credits {
2599 foreach ( $self->cust_credited ) {
2601 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2603 my $reason = $_->cust_credit->reason;
2604 #my $reason = substr($_->cust_credit->reason,0,32);
2605 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2606 $reason = " ($reason) " if $reason;
2608 #'description' => 'Credit ref\#'. $_->crednum.
2609 # " (". time2str("%x",$_->cust_credit->_date) .")".
2611 'description' => 'Credit applied '.
2612 time2str("%x",$_->cust_credit->_date). $reason,
2613 'amount' => sprintf("%.2f",$_->amount),
2616 #foreach ( @cr_cust_credit ) {
2618 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2619 # $money_char. sprintf("%10.2f",$_->credited)
2627 sub _items_payments {
2631 #get & print payments
2632 foreach ( $self->cust_bill_pay ) {
2634 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2637 'description' => "Payment received ".
2638 time2str("%x",$_->cust_pay->_date ),
2639 'amount' => sprintf("%.2f", $_->amount )
2658 sub process_reprint {
2659 process_re_X('print', @_);
2666 sub process_reemail {
2667 process_re_X('email', @_);
2675 process_re_X('fax', @_);
2678 use Storable qw(thaw);
2682 my( $method, $job ) = ( shift, shift );
2683 warn "process_re_X $method for job $job\n" if $DEBUG;
2685 my $param = thaw(decode_base64(shift));
2686 warn Dumper($param) if $DEBUG;
2697 my($method, $job, %param ) = @_;
2698 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2700 warn "re_X $method for job $job with param:\n".
2701 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2704 #some false laziness w/search/cust_bill.html
2706 my $orderby = 'ORDER BY cust_bill._date';
2710 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2711 push @where, "cust_bill._date >= $1";
2713 if ( $param{'end'} =~ /^(\d+)$/ ) {
2714 push @where, "cust_bill._date < $1";
2716 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2717 push @where, "cust_main.agentnum = $1";
2721 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2722 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2723 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2724 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2726 push @where, "0 != $owed"
2729 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2732 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2734 my $addl_from = 'left join cust_main using ( custnum )';
2736 if ( $param{'newest_percust'} ) {
2737 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2738 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2739 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2742 my @cust_bill = qsearch( 'cust_bill',
2744 "$distinct cust_bill.*",
2750 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2751 foreach my $cust_bill ( @cust_bill ) {
2752 $cust_bill->$method();
2754 if ( $job ) { #progressbar foo
2756 if ( time - $min_sec > $last ) {
2757 my $error = $job->update_statustext(
2758 int( 100 * $num / scalar(@cust_bill) )
2760 die $error if $error;
2775 print_text formatting (and some logic :/) is in source, but needs to be
2776 slurped in from a file. Also number of lines ($=).
2780 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2781 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base