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 @args = ( $opt{template}, $opt{agentnum} );
739 push @args, $opt{invoice_from}
740 if exists($opt{invoice_from}) && $opt{invoice_from};
742 my $error = $self->send( @args );
743 die $error if $error;
749 my $template = scalar(@_) ? shift : '';
750 if ( scalar(@_) && $_[0] ) {
751 my $agentnums = ref($_[0]) ? shift : [ shift ];
752 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
758 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
760 my @invoicing_list = $self->cust_main->invoicing_list;
762 $self->email($template, $invoice_from)
763 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
765 $self->print($template)
766 if grep { $_ eq 'POST' } @invoicing_list; #postal
768 $self->fax($template)
769 if grep { $_ eq 'FAX' } @invoicing_list; #fax
775 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
779 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
781 INVOICE_FROM, if specified, overrides the default email invoice From: address.
785 sub queueable_email {
788 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
789 or die "invalid invoice number: " . $opt{invnum};
791 my @args = ( $opt{template} );
792 push @args, $opt{invoice_from}
793 if exists($opt{invoice_from}) && $opt{invoice_from};
795 my $error = $self->email( @args );
796 die $error if $error;
802 my $template = scalar(@_) ? shift : '';
806 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
808 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
809 $self->cust_main->invoicing_list;
811 #better to notify this person than silence
812 @invoicing_list = ($invoice_from) unless @invoicing_list;
814 my $error = send_email(
815 $self->generate_email(
816 'from' => $invoice_from,
817 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
818 'template' => $template,
821 die "can't email invoice: $error\n" if $error;
822 #die "$error\n" if $error;
826 =item lpr_data [ TEMPLATENAME ]
828 Returns the postscript or plaintext for this invoice as an arrayref.
830 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
835 my( $self, $template) = @_;
836 $conf->exists('invoice_latex')
837 ? [ $self->print_ps('', $template) ]
838 : [ $self->print_text('', $template) ];
841 =item print [ TEMPLATENAME ]
845 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
851 my $template = scalar(@_) ? shift : '';
853 my $lpr = $conf->config('lpr');
856 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
858 $outerr = ": $outerr" if length($outerr);
859 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
864 =item fax [ TEMPLATENAME ]
868 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
874 my $template = scalar(@_) ? shift : '';
876 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
877 unless $conf->exists('invoice_latex');
879 my $dialstring = $self->cust_main->getfield('fax');
882 my $error = send_fax( 'docdata' => $self->lpr_data($template),
883 'dialstring' => $dialstring,
885 die $error if $error;
889 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
891 Like B<send>, but only sends the invoice if it is the newest open invoice for
901 grep { $_->owed > 0 }
902 qsearch('cust_bill', {
903 'custnum' => $self->custnum,
904 #'_date' => { op=>'>', value=>$self->_date },
905 'invnum' => { op=>'>', value=>$self->invnum },
912 =item send_csv OPTION => VALUE, ...
914 Sends invoice as a CSV data-file to a remote host with the specified protocol.
918 protocol - currently only "ftp"
924 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
925 and YYMMDDHHMMSS is a timestamp.
927 See L</print_csv> for a description of the output format.
932 my($self, %opt) = @_;
936 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
937 mkdir $spooldir, 0700 unless -d $spooldir;
939 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
940 my $file = "$spooldir/$tracctnum.csv";
942 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
944 open(CSV, ">$file") or die "can't open $file: $!";
952 if ( $opt{protocol} eq 'ftp' ) {
953 eval "use Net::FTP;";
955 $net = Net::FTP->new($opt{server}) or die @$;
957 die "unknown protocol: $opt{protocol}";
960 $net->login( $opt{username}, $opt{password} )
961 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
963 $net->binary or die "can't set binary mode";
965 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
967 $net->put($file) or die "can't put $file: $!";
977 Spools CSV invoice data.
983 =item format - 'default' or 'billco'
985 =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>).
987 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
989 =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.
996 my($self, %opt) = @_;
998 my $cust_main = $self->cust_main;
1000 if ( $opt{'dest'} ) {
1001 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1002 $cust_main->invoicing_list;
1003 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1004 || ! keys %invoicing_list;
1007 if ( $opt{'balanceover'} ) {
1009 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1012 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1013 mkdir $spooldir, 0700 unless -d $spooldir;
1015 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1019 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1020 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1023 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1025 open(CSV, ">>$file") or die "can't open $file: $!";
1026 flock(CSV, LOCK_EX);
1031 if ( lc($opt{'format'}) eq 'billco' ) {
1033 flock(CSV, LOCK_UN);
1038 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1041 open(CSV,">>$file") or die "can't open $file: $!";
1042 flock(CSV, LOCK_EX);
1048 flock(CSV, LOCK_UN);
1055 =item print_csv OPTION => VALUE, ...
1057 Returns CSV data for this invoice.
1061 format - 'default' or 'billco'
1063 Returns a list consisting of two scalars. The first is a single line of CSV
1064 header information for this invoice. The second is one or more lines of CSV
1065 detail information for this invoice.
1067 If I<format> is not specified or "default", the fields of the CSV file are as
1070 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1074 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1076 B<record_type> is C<cust_bill> for the initial header line only. The
1077 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1078 fields are filled in.
1080 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1081 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1084 =item invnum - invoice number
1086 =item custnum - customer number
1088 =item _date - invoice date
1090 =item charged - total invoice amount
1092 =item first - customer first name
1094 =item last - customer first name
1096 =item company - company name
1098 =item address1 - address line 1
1100 =item address2 - address line 1
1110 =item pkg - line item description
1112 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1114 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1116 =item sdate - start date for recurring fee
1118 =item edate - end date for recurring fee
1122 If I<format> is "billco", the fields of the header CSV file are as follows:
1124 +-------------------------------------------------------------------+
1125 | FORMAT HEADER FILE |
1126 |-------------------------------------------------------------------|
1127 | Field | Description | Name | Type | Width |
1128 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1129 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1130 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1131 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1132 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1133 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1134 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1135 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1136 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1137 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1138 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1139 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1140 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1141 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1142 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1143 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1144 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1145 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1146 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1147 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1148 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1149 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1150 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1151 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1152 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1153 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1154 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1155 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1156 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1157 +-------+-------------------------------+------------+------+-------+
1159 If I<format> is "billco", the fields of the detail CSV file are as follows:
1161 FORMAT FOR DETAIL FILE
1163 Field | Description | Name | Type | Width
1164 1 | N/A-Leave Empty | RC | CHAR | 2
1165 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1166 3 | Account Number | TRACCTNUM | CHAR | 15
1167 4 | Invoice Number | TRINVOICE | CHAR | 15
1168 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1169 6 | Transaction Detail | DETAILS | CHAR | 100
1170 7 | Amount | AMT | NUM* | 9
1171 8 | Line Format Control** | LNCTRL | CHAR | 2
1172 9 | Grouping Code | GROUP | CHAR | 2
1173 10 | User Defined | ACCT CODE | CHAR | 15
1178 my($self, %opt) = @_;
1180 eval "use Text::CSV_XS";
1183 my $cust_main = $self->cust_main;
1185 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1187 if ( lc($opt{'format'}) eq 'billco' ) {
1190 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1193 if ( $conf->exists('invoice_default_terms')
1194 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1195 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1198 my( $previous_balance, @unused ) = $self->previous; #previous balance
1200 my $pmt_cr_applied = 0;
1201 $pmt_cr_applied += $_->{'amount'}
1202 foreach ( $self->_items_payments, $self->_items_credits ) ;
1204 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1207 '', # 1 | N/A-Leave Empty CHAR 2
1208 '', # 2 | N/A-Leave Empty CHAR 15
1209 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1210 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1211 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1212 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1213 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1214 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1215 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1216 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1217 '', # 10 | Ancillary Billing Information CHAR 30
1218 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1219 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1222 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1225 $duedate, # 14 | Bill Due Date CHAR 10
1227 $previous_balance, # 15 | Previous Balance NUM* 9
1228 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1229 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1230 $totaldue, # 18 | Total Amt Due NUM* 9
1231 $totaldue, # 19 | Total Amt Due NUM* 9
1232 '', # 20 | 30 Day Aging NUM* 9
1233 '', # 21 | 60 Day Aging NUM* 9
1234 '', # 22 | 90 Day Aging NUM* 9
1235 'N', # 23 | Y/N CHAR 1
1236 '', # 24 | Remittance automation CHAR 100
1237 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1238 $self->custnum, # 26 | Customer Reference Number CHAR 15
1239 '0', # 27 | Federal Tax*** NUM* 9
1240 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1241 '0', # 29 | Other Taxes & Fees*** NUM* 9
1250 time2str("%x", $self->_date),
1251 sprintf("%.2f", $self->charged),
1252 ( map { $cust_main->getfield($_) }
1253 qw( first last company address1 address2 city state zip country ) ),
1255 ) or die "can't create csv";
1258 my $header = $csv->string. "\n";
1261 if ( lc($opt{'format'}) eq 'billco' ) {
1264 foreach my $item ( $self->_items_pkg ) {
1267 '', # 1 | N/A-Leave Empty CHAR 2
1268 '', # 2 | N/A-Leave Empty CHAR 15
1269 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1270 $self->invnum, # 4 | Invoice Number CHAR 15
1271 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1272 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1273 $item->{'amount'}, # 7 | Amount NUM* 9
1274 '', # 8 | Line Format Control** CHAR 2
1275 '', # 9 | Grouping Code CHAR 2
1276 '', # 10 | User Defined CHAR 15
1279 $detail .= $csv->string. "\n";
1285 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1287 my($pkg, $setup, $recur, $sdate, $edate);
1288 if ( $cust_bill_pkg->pkgnum ) {
1290 ($pkg, $setup, $recur, $sdate, $edate) = (
1291 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1292 ( $cust_bill_pkg->setup != 0
1293 ? sprintf("%.2f", $cust_bill_pkg->setup )
1295 ( $cust_bill_pkg->recur != 0
1296 ? sprintf("%.2f", $cust_bill_pkg->recur )
1298 ( $cust_bill_pkg->sdate
1299 ? time2str("%x", $cust_bill_pkg->sdate)
1301 ($cust_bill_pkg->edate
1302 ?time2str("%x", $cust_bill_pkg->edate)
1306 } else { #pkgnum tax
1307 next unless $cust_bill_pkg->setup != 0;
1308 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1309 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1311 ($pkg, $setup, $recur, $sdate, $edate) =
1312 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1318 ( map { '' } (1..11) ),
1319 ($pkg, $setup, $recur, $sdate, $edate)
1320 ) or die "can't create csv";
1322 $detail .= $csv->string. "\n";
1328 ( $header, $detail );
1334 Pays this invoice with a compliemntary payment. If there is an error,
1335 returns the error, otherwise returns false.
1341 my $cust_pay = new FS::cust_pay ( {
1342 'invnum' => $self->invnum,
1343 'paid' => $self->owed,
1346 'payinfo' => $self->cust_main->payinfo,
1354 Attempts to pay this invoice with a credit card payment via a
1355 Business::OnlinePayment realtime gateway. See
1356 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1357 for supported processors.
1363 $self->realtime_bop( 'CC', @_ );
1368 Attempts to pay this invoice with an electronic check (ACH) payment via a
1369 Business::OnlinePayment realtime gateway. See
1370 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1371 for supported processors.
1377 $self->realtime_bop( 'ECHECK', @_ );
1382 Attempts to pay this invoice with phone bill (LEC) payment via a
1383 Business::OnlinePayment realtime gateway. See
1384 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1385 for supported processors.
1391 $self->realtime_bop( 'LEC', @_ );
1395 my( $self, $method ) = @_;
1397 my $cust_main = $self->cust_main;
1398 my $balance = $cust_main->balance;
1399 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1400 $amount = sprintf("%.2f", $amount);
1401 return "not run (balance $balance)" unless $amount > 0;
1403 my $description = 'Internet Services';
1404 if ( $conf->exists('business-onlinepayment-description') ) {
1405 my $dtempl = $conf->config('business-onlinepayment-description');
1407 my $agent_obj = $cust_main->agent
1408 or die "can't retreive agent for $cust_main (agentnum ".
1409 $cust_main->agentnum. ")";
1410 my $agent = $agent_obj->agent;
1411 my $pkgs = join(', ',
1412 map { $_->cust_pkg->part_pkg->pkg }
1413 grep { $_->pkgnum } $self->cust_bill_pkg
1415 $description = eval qq("$dtempl");
1418 $cust_main->realtime_bop($method, $amount,
1419 'description' => $description,
1420 'invnum' => $self->invnum,
1425 =item batch_card OPTION => VALUE...
1427 Adds a payment for this invoice to the pending credit card batch (see
1428 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1429 runs the payment using a realtime gateway.
1434 my ($self, %options) = @_;
1435 my $cust_main = $self->cust_main;
1437 my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1438 return '' unless $amount > 0;
1440 if ($options{'realtime'}) {
1441 return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1447 my $oldAutoCommit = $FS::UID::AutoCommit;
1448 local $FS::UID::AutoCommit = 0;
1451 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1452 or return "Cannot lock pay_batch: " . $dbh->errstr;
1456 'payby' => FS::payby->payby2payment($cust_main->payby),
1459 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1461 unless ( $pay_batch ) {
1462 $pay_batch = new FS::pay_batch \%pay_batch;
1463 my $error = $pay_batch->insert;
1465 $dbh->rollback if $oldAutoCommit;
1466 die "error creating new batch: $error\n";
1470 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1471 'batchnum' => $pay_batch->batchnum,
1472 'custnum' => $cust_main->custnum,
1475 my $cust_pay_batch = new FS::cust_pay_batch ( {
1476 'batchnum' => $pay_batch->batchnum,
1477 'invnum' => $self->getfield('invnum'), # is there a better value?
1478 # this field should be
1480 # cust_bill_pay_batch now
1481 'custnum' => $cust_main->custnum,
1482 'last' => $cust_main->getfield('last'),
1483 'first' => $cust_main->getfield('first'),
1484 'address1' => $cust_main->address1,
1485 'address2' => $cust_main->address2,
1486 'city' => $cust_main->city,
1487 'state' => $cust_main->state,
1488 'zip' => $cust_main->zip,
1489 'country' => $cust_main->country,
1490 'payby' => $cust_main->payby,
1491 'payinfo' => $cust_main->payinfo,
1492 'exp' => $cust_main->paydate,
1493 'payname' => $cust_main->payname,
1494 'amount' => $amount, # consolidating
1497 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1498 if $old_cust_pay_batch;
1501 if ($old_cust_pay_batch) {
1502 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1504 $error = $cust_pay_batch->insert;
1508 $dbh->rollback if $oldAutoCommit;
1512 my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1513 foreach my $cust_bill ($cust_main->open_cust_bill) {
1514 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1515 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1516 'invnum' => $cust_bill->invnum,
1517 'paybatchnum' => $cust_pay_batch->paybatchnum,
1518 'amount' => $cust_bill->owed,
1521 if ($unapplied >= $cust_bill_pay_batch->amount){
1522 $unapplied -= $cust_bill_pay_batch->amount;
1525 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
1526 $cust_bill_pay_batch->amount - $unapplied ));
1529 $error = $cust_bill_pay_batch->insert;
1531 $dbh->rollback if $oldAutoCommit;
1536 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1540 sub _agent_template {
1542 $self->_agent_plandata('agent_templatename');
1545 sub _agent_invoice_from {
1547 $self->_agent_plandata('agent_invoice_from');
1550 sub _agent_plandata {
1551 my( $self, $option ) = @_;
1553 my $part_bill_event = qsearchs( 'part_bill_event',
1555 'payby' => $self->cust_main->payby,
1556 'plan' => 'send_agent',
1557 'plandata' => { 'op' => '~',
1558 'value' => "(^|\n)agentnum ".
1560 $self->cust_main->agentnum.
1566 'ORDER BY seconds LIMIT 1'
1569 return '' unless $part_bill_event;
1571 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1574 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1575 " plandata for $option";
1581 =item print_text [ TIME [ , TEMPLATE ] ]
1583 Returns an text invoice, as a list of lines.
1585 TIME an optional value used to control the printing of overdue messages. The
1586 default is now. It isn't the date of the invoice; that's the `_date' field.
1587 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1588 L<Time::Local> and L<Date::Parse> for conversion functions.
1592 #still some false laziness w/_items stuff (and send_csv)
1595 my( $self, $today, $template ) = @_;
1598 # my $invnum = $self->invnum;
1599 my $cust_main = $self->cust_main;
1600 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1601 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1603 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1604 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1605 #my $balance_due = $self->owed + $pr_total - $cr_total;
1606 my $balance_due = $self->owed + $pr_total;
1609 #my($description,$amount);
1613 foreach ( @pr_cust_bill ) {
1615 "Previous Balance, Invoice #". $_->invnum.
1616 " (". time2str("%x",$_->_date). ")",
1617 $money_char. sprintf("%10.2f",$_->owed)
1620 if (@pr_cust_bill) {
1621 push @buf,['','-----------'];
1622 push @buf,[ 'Total Previous Balance',
1623 $money_char. sprintf("%10.2f",$pr_total ) ];
1628 foreach my $cust_bill_pkg (
1629 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1630 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1633 my $desc = $cust_bill_pkg->desc;
1635 if ( $cust_bill_pkg->pkgnum > 0 ) {
1637 if ( $cust_bill_pkg->setup != 0 ) {
1638 my $description = $desc;
1639 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1640 push @buf, [ $description,
1641 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1643 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1644 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1647 if ( $cust_bill_pkg->recur != 0 ) {
1649 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1650 time2str("%x", $cust_bill_pkg->edate) . ")",
1651 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1654 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1655 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1656 $cust_bill_pkg->sdate );
1659 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1661 } else { #pkgnum tax or one-shot line item
1663 if ( $cust_bill_pkg->setup != 0 ) {
1665 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1667 if ( $cust_bill_pkg->recur != 0 ) {
1668 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1669 . time2str("%x", $cust_bill_pkg->edate). ")",
1670 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1678 push @buf,['','-----------'];
1679 push @buf,['Total New Charges',
1680 $money_char. sprintf("%10.2f",$self->charged) ];
1683 push @buf,['','-----------'];
1684 push @buf,['Total Charges',
1685 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1689 foreach ( $self->cust_credited ) {
1691 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1693 my $reason = substr($_->cust_credit->reason,0,32);
1694 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1695 $reason = " ($reason) " if $reason;
1697 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1699 $money_char. sprintf("%10.2f",$_->amount)
1702 #foreach ( @cr_cust_credit ) {
1704 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1705 # $money_char. sprintf("%10.2f",$_->credited)
1709 #get & print payments
1710 foreach ( $self->cust_bill_pay ) {
1712 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1715 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1716 $money_char. sprintf("%10.2f",$_->amount )
1721 my $balance_due_msg = $self->balance_due_msg;
1723 push @buf,['','-----------'];
1724 push @buf,[$balance_due_msg, $money_char.
1725 sprintf("%10.2f", $balance_due ) ];
1727 #create the template
1728 $template ||= $self->_agent_template;
1729 my $templatefile = 'invoice_template';
1730 $templatefile .= "_$template" if length($template);
1731 my @invoice_template = $conf->config($templatefile)
1732 or die "cannot load config file $templatefile";
1735 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1736 /invoice_lines\((\d*)\)/;
1737 $invoice_lines += $1 || scalar(@buf);
1740 die "no invoice_lines() functions in template?" unless $wasfunc;
1741 my $invoice_template = new Text::Template (
1743 SOURCE => [ map "$_\n", @invoice_template ],
1744 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1745 $invoice_template->compile()
1746 or die "can't compile template: $Text::Template::ERROR";
1748 #setup template variables
1749 package FS::cust_bill::_template; #!
1750 use vars qw( $custnum $invnum $date $agent @address $overdue
1751 $page $total_pages @buf );
1753 $custnum = $self->custnum;
1754 $invnum = $self->invnum;
1755 $date = $self->_date;
1756 $agent = $self->cust_main->agent->agent;
1759 if ( $FS::cust_bill::invoice_lines ) {
1761 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1763 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1768 #format address (variable for the template)
1770 @address = ( '', '', '', '', '', '' );
1771 package FS::cust_bill; #!
1772 $FS::cust_bill::_template::address[$l++] =
1773 $cust_main->payname.
1774 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1775 ? " (P.O. #". $cust_main->payinfo. ")"
1779 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1780 if $cust_main->company;
1781 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1782 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1783 if $cust_main->address2;
1784 $FS::cust_bill::_template::address[$l++] =
1785 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1787 my $countrydefault = $conf->config('countrydefault') || 'US';
1788 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1789 unless $cust_main->country eq $countrydefault;
1791 # #overdue? (variable for the template)
1792 # $FS::cust_bill::_template::overdue = (
1794 # && $today > $self->_date
1795 ## && $self->printed > 1
1796 # && $self->printed > 0
1799 #and subroutine for the template
1800 sub FS::cust_bill::_template::invoice_lines {
1801 my $lines = shift || scalar(@buf);
1803 scalar(@buf) ? shift @buf : [ '', '' ];
1809 $FS::cust_bill::_template::page = 1;
1813 push @collect, split("\n",
1814 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1816 $FS::cust_bill::_template::page++;
1819 map "$_\n", @collect;
1823 =item print_latex [ TIME [ , TEMPLATE ] ]
1825 Internal method - returns a filename of a filled-in LaTeX template for this
1826 invoice (Note: add ".tex" to get the actual filename).
1828 See print_ps and print_pdf for methods that return PostScript and PDF output.
1830 TIME an optional value used to control the printing of overdue messages. The
1831 default is now. It isn't the date of the invoice; that's the `_date' field.
1832 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1833 L<Time::Local> and L<Date::Parse> for conversion functions.
1837 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1840 my( $self, $today, $template ) = @_;
1842 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1845 my $cust_main = $self->cust_main;
1846 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1847 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1849 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1850 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1851 #my $balance_due = $self->owed + $pr_total - $cr_total;
1852 my $balance_due = $self->owed + $pr_total;
1854 #create the template
1855 $template ||= $self->_agent_template;
1856 my $templatefile = 'invoice_latex';
1857 my $suffix = length($template) ? "_$template" : '';
1858 $templatefile .= $suffix;
1859 my @invoice_template = map "$_\n", $conf->config($templatefile)
1860 or die "cannot load config file $templatefile";
1862 my($format, $text_template);
1863 if ( grep { /^%%Detail/ } @invoice_template ) {
1864 #change this to a die when the old code is removed
1865 warn "old-style invoice template $templatefile; ".
1866 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1869 $format = 'Text::Template';
1870 $text_template = new Text::Template(
1872 SOURCE => \@invoice_template,
1873 DELIMITERS => [ '[@--', '--@]' ],
1876 $text_template->compile()
1877 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1881 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1882 $returnaddress = join("\n",
1883 $conf->config_orbase('invoice_latexreturnaddress', $template)
1886 $returnaddress = '~';
1889 my %invoice_data = (
1890 'custnum' => $self->custnum,
1891 'invnum' => $self->invnum,
1892 'date' => time2str('%b %o, %Y', $self->_date),
1893 'today' => time2str('%b %o, %Y', $today),
1894 'agent' => _latex_escape($cust_main->agent->agent),
1895 'payname' => _latex_escape($cust_main->payname),
1896 'company' => _latex_escape($cust_main->company),
1897 'address1' => _latex_escape($cust_main->address1),
1898 'address2' => _latex_escape($cust_main->address2),
1899 'city' => _latex_escape($cust_main->city),
1900 'state' => _latex_escape($cust_main->state),
1901 'zip' => _latex_escape($cust_main->zip),
1902 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1903 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1904 'returnaddress' => $returnaddress,
1906 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1907 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1908 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1911 my $countrydefault = $conf->config('countrydefault') || 'US';
1912 if ( $cust_main->country eq $countrydefault ) {
1913 $invoice_data{'country'} = '';
1915 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1918 $invoice_data{'notes'} =
1920 # #do variable substitutions in notes
1921 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1922 $conf->config_orbase('invoice_latexnotes', $template)
1924 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1927 $invoice_data{'footer'} =~ s/\n+$//;
1928 $invoice_data{'smallfooter'} =~ s/\n+$//;
1929 $invoice_data{'notes'} =~ s/\n+$//;
1931 $invoice_data{'po_line'} =
1932 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1933 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1937 if ( $format eq 'old' ) {
1940 my @total_item = ();
1941 while ( @invoice_template ) {
1942 my $line = shift @invoice_template;
1944 if ( $line =~ /^%%Detail\s*$/ ) {
1946 while ( ( my $line_item_line = shift @invoice_template )
1947 !~ /^%%EndDetail\s*$/ ) {
1948 push @line_item, $line_item_line;
1950 foreach my $line_item ( $self->_items ) {
1951 #foreach my $line_item ( $self->_items_pkg ) {
1952 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1953 $invoice_data{'description'} =
1954 _latex_escape($line_item->{'description'});
1955 if ( exists $line_item->{'ext_description'} ) {
1956 $invoice_data{'description'} .=
1957 "\\tabularnewline\n~~".
1958 join( "\\tabularnewline\n~~",
1959 map _latex_escape($_), @{$line_item->{'ext_description'}}
1962 $invoice_data{'amount'} = $line_item->{'amount'};
1963 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1965 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1968 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1970 while ( ( my $total_item_line = shift @invoice_template )
1971 !~ /^%%EndTotalDetails\s*$/ ) {
1972 push @total_item, $total_item_line;
1975 my @total_fill = ();
1978 foreach my $tax ( $self->_items_tax ) {
1979 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1980 $taxtotal += $tax->{'amount'};
1981 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1983 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1988 $invoice_data{'total_item'} = 'Sub-total';
1989 $invoice_data{'total_amount'} =
1990 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1991 unshift @total_fill,
1992 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1996 $invoice_data{'total_item'} = '\textbf{Total}';
1997 $invoice_data{'total_amount'} =
1998 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2000 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2003 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2006 foreach my $credit ( $self->_items_credits ) {
2007 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
2009 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
2011 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2016 foreach my $payment ( $self->_items_payments ) {
2017 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
2019 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
2021 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2025 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2026 $invoice_data{'total_amount'} =
2027 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2029 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2032 push @filled_in, @total_fill;
2035 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2036 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2037 push @filled_in, $line;
2048 } elsif ( $format eq 'Text::Template' ) {
2050 my @detail_items = ();
2051 my @total_items = ();
2053 $invoice_data{'detail_items'} = \@detail_items;
2054 $invoice_data{'total_items'} = \@total_items;
2056 foreach my $line_item ( $self->_items ) {
2058 ext_description => [],
2060 $detail->{'ref'} = $line_item->{'pkgnum'};
2061 $detail->{'quantity'} = 1;
2062 $detail->{'description'} = _latex_escape($line_item->{'description'});
2063 if ( exists $line_item->{'ext_description'} ) {
2064 @{$detail->{'ext_description'}} = map {
2066 } @{$line_item->{'ext_description'}};
2068 $detail->{'amount'} = $line_item->{'amount'};
2069 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2071 push @detail_items, $detail;
2076 foreach my $tax ( $self->_items_tax ) {
2078 $total->{'total_item'} = _latex_escape($tax->{'description'});
2079 $taxtotal += $tax->{'amount'};
2080 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2081 push @total_items, $total;
2086 $total->{'total_item'} = 'Sub-total';
2087 $total->{'total_amount'} =
2088 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2089 unshift @total_items, $total;
2094 $total->{'total_item'} = '\textbf{Total}';
2095 $total->{'total_amount'} =
2096 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2097 push @total_items, $total;
2100 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2103 foreach my $credit ( $self->_items_credits ) {
2105 $total->{'total_item'} = _latex_escape($credit->{'description'});
2107 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2108 push @total_items, $total;
2112 foreach my $payment ( $self->_items_payments ) {
2114 $total->{'total_item'} = _latex_escape($payment->{'description'});
2116 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2117 push @total_items, $total;
2122 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2123 $total->{'total_amount'} =
2124 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2125 push @total_items, $total;
2129 die "guru meditation #54";
2132 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2133 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2137 ) or die "can't open temp file: $!\n";
2138 if ( $format eq 'old' ) {
2139 print $fh join('', @filled_in );
2140 } elsif ( $format eq 'Text::Template' ) {
2141 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2143 die "guru meditation #32";
2147 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2152 =item print_ps [ TIME [ , TEMPLATE ] ]
2154 Returns an postscript invoice, as a scalar.
2156 TIME an optional value used to control the printing of overdue messages. The
2157 default is now. It isn't the date of the invoice; that's the `_date' field.
2158 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2159 L<Time::Local> and L<Date::Parse> for conversion functions.
2166 my $file = $self->print_latex(@_);
2168 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2171 my $sfile = shell_quote $file;
2173 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2174 or die "pslatex $file.tex failed; see $file.log for details?\n";
2175 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2176 or die "pslatex $file.tex failed; see $file.log for details?\n";
2178 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2179 or die "dvips failed";
2181 open(POSTSCRIPT, "<$file.ps")
2182 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2184 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2187 while (<POSTSCRIPT>) {
2197 =item print_pdf [ TIME [ , TEMPLATE ] ]
2199 Returns an PDF invoice, as a scalar.
2201 TIME an optional value used to control the printing of overdue messages. The
2202 default is now. It isn't the date of the invoice; that's the `_date' field.
2203 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2204 L<Time::Local> and L<Date::Parse> for conversion functions.
2211 my $file = $self->print_latex(@_);
2213 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2216 #system('pdflatex', "$file.tex");
2217 #system('pdflatex', "$file.tex");
2218 #! LaTeX Error: Unknown graphics extension: .eps.
2220 my $sfile = shell_quote $file;
2222 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2223 or die "pslatex $file.tex failed; see $file.log for details?\n";
2224 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2225 or die "pslatex $file.tex failed; see $file.log for details?\n";
2227 #system('dvipdf', "$file.dvi", "$file.pdf" );
2229 "dvips -q -t letter -f $sfile.dvi ".
2230 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2233 or die "dvips | gs failed: $!";
2235 open(PDF, "<$file.pdf")
2236 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2238 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2251 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2253 Returns an HTML 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.
2260 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2261 when emailing the invoice as part of a multipart/related MIME email.
2265 #some falze laziness w/print_text and print_latex (and send_csv)
2267 my( $self, $today, $template, $cid ) = @_;
2270 my $cust_main = $self->cust_main;
2271 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2272 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2274 $template ||= $self->_agent_template;
2275 my $templatefile = 'invoice_html';
2276 my $suffix = length($template) ? "_$template" : '';
2277 $templatefile .= $suffix;
2278 my @html_template = map "$_\n", $conf->config($templatefile)
2279 or die "cannot load config file $templatefile";
2281 my $html_template = new Text::Template(
2283 SOURCE => \@html_template,
2284 DELIMITERS => [ '<%=', '%>' ],
2287 $html_template->compile()
2288 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2290 my %invoice_data = (
2291 'custnum' => $self->custnum,
2292 'invnum' => $self->invnum,
2293 'date' => time2str('%b %o, %Y', $self->_date),
2294 'today' => time2str('%b %o, %Y', $today),
2295 'agent' => encode_entities($cust_main->agent->agent),
2296 'payname' => encode_entities($cust_main->payname),
2297 'company' => encode_entities($cust_main->company),
2298 'address1' => encode_entities($cust_main->address1),
2299 'address2' => encode_entities($cust_main->address2),
2300 'city' => encode_entities($cust_main->city),
2301 'state' => encode_entities($cust_main->state),
2302 'zip' => encode_entities($cust_main->zip),
2303 'terms' => $conf->config('invoice_default_terms')
2304 || 'Payable upon receipt',
2306 'template' => $template,
2307 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2311 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2312 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2314 $invoice_data{'returnaddress'} =
2315 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2317 $invoice_data{'returnaddress'} =
2320 s/\\\\\*?\s*$/<BR>/;
2321 s/\\hyphenation\{[\w\s\-]+\}//;
2324 $conf->config_orbase( 'invoice_latexreturnaddress',
2330 my $countrydefault = $conf->config('countrydefault') || 'US';
2331 if ( $cust_main->country eq $countrydefault ) {
2332 $invoice_data{'country'} = '';
2334 $invoice_data{'country'} =
2335 encode_entities(code2country($cust_main->country));
2339 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2340 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2342 $invoice_data{'notes'} =
2343 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2345 $invoice_data{'notes'} =
2347 s/%%(.*)$/<!-- $1 -->/;
2348 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2349 s/\\begin\{enumerate\}/<ol>/;
2351 s/\\end\{enumerate\}/<\/ol>/;
2352 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2356 $conf->config_orbase('invoice_latexnotes', $template)
2360 # #do variable substitutions in notes
2361 # $invoice_data{'notes'} =
2363 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2364 # $conf->config_orbase('invoice_latexnotes', $suffix)
2368 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2369 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2371 $invoice_data{'footer'} =
2372 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2374 $invoice_data{'footer'} =
2375 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2376 $conf->config_orbase('invoice_latexfooter', $template)
2380 $invoice_data{'po_line'} =
2381 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2382 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2385 my $money_char = $conf->config('money_char') || '$';
2387 foreach my $line_item ( $self->_items ) {
2389 ext_description => [],
2391 $detail->{'ref'} = $line_item->{'pkgnum'};
2392 $detail->{'description'} = encode_entities($line_item->{'description'});
2393 if ( exists $line_item->{'ext_description'} ) {
2394 @{$detail->{'ext_description'}} = map {
2395 encode_entities($_);
2396 } @{$line_item->{'ext_description'}};
2398 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2399 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2401 push @{$invoice_data{'detail_items'}}, $detail;
2406 foreach my $tax ( $self->_items_tax ) {
2408 $total->{'total_item'} = encode_entities($tax->{'description'});
2409 $taxtotal += $tax->{'amount'};
2410 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2411 push @{$invoice_data{'total_items'}}, $total;
2416 $total->{'total_item'} = 'Sub-total';
2417 $total->{'total_amount'} =
2418 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2419 unshift @{$invoice_data{'total_items'}}, $total;
2422 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2425 $total->{'total_item'} = '<b>Total</b>';
2426 $total->{'total_amount'} =
2427 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2428 push @{$invoice_data{'total_items'}}, $total;
2431 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2434 foreach my $credit ( $self->_items_credits ) {
2436 $total->{'total_item'} = encode_entities($credit->{'description'});
2438 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2439 push @{$invoice_data{'total_items'}}, $total;
2443 foreach my $payment ( $self->_items_payments ) {
2445 $total->{'total_item'} = encode_entities($payment->{'description'});
2447 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2448 push @{$invoice_data{'total_items'}}, $total;
2453 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2454 $total->{'total_amount'} =
2455 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2456 push @{$invoice_data{'total_items'}}, $total;
2459 $html_template->fill_in( HASH => \%invoice_data);
2462 # quick subroutine for print_latex
2464 # There are ten characters that LaTeX treats as special characters, which
2465 # means that they do not simply typeset themselves:
2466 # # $ % & ~ _ ^ \ { }
2468 # TeX ignores blanks following an escaped character; if you want a blank (as
2469 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2473 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2474 $value =~ s/([<>])/\$$1\$/g;
2478 #utility methods for print_*
2480 sub balance_due_msg {
2482 my $msg = 'Balance Due';
2483 return $msg unless $conf->exists('invoice_default_terms');
2484 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2485 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2486 } elsif ( $conf->config('invoice_default_terms') ) {
2487 $msg .= ' - '. $conf->config('invoice_default_terms');
2494 my @display = scalar(@_)
2496 : qw( _items_previous _items_pkg );
2497 #: qw( _items_pkg );
2498 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2500 foreach my $display ( @display ) {
2501 push @b, $self->$display(@_);
2506 sub _items_previous {
2508 my $cust_main = $self->cust_main;
2509 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2511 foreach ( @pr_cust_bill ) {
2513 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2514 ' ('. time2str('%x',$_->_date). ')',
2515 #'pkgpart' => 'N/A',
2517 'amount' => sprintf("%.2f", $_->owed),
2523 # 'description' => 'Previous Balance',
2524 # #'pkgpart' => 'N/A',
2525 # 'pkgnum' => 'N/A',
2526 # 'amount' => sprintf("%10.2f", $pr_total ),
2527 # 'ext_description' => [ map {
2528 # "Invoice ". $_->invnum.
2529 # " (". time2str("%x",$_->_date). ") ".
2530 # sprintf("%10.2f", $_->owed)
2531 # } @pr_cust_bill ],
2538 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2539 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2544 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2545 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2548 sub _items_cust_bill_pkg {
2550 my $cust_bill_pkg = shift;
2553 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2555 my $desc = $cust_bill_pkg->desc;
2557 if ( $cust_bill_pkg->pkgnum > 0 ) {
2559 if ( $cust_bill_pkg->setup != 0 ) {
2560 my $description = $desc;
2561 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2562 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2563 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2565 description => $description,
2566 #pkgpart => $part_pkg->pkgpart,
2567 pkgnum => $cust_bill_pkg->pkgnum,
2568 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2569 ext_description => \@d,
2573 if ( $cust_bill_pkg->recur != 0 ) {
2575 description => "$desc (" .
2576 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2577 time2str('%x', $cust_bill_pkg->edate). ')',
2578 #pkgpart => $part_pkg->pkgpart,
2579 pkgnum => $cust_bill_pkg->pkgnum,
2580 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2582 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2583 $cust_bill_pkg->sdate),
2584 $cust_bill_pkg->details,
2589 } else { #pkgnum tax or one-shot line item (??)
2591 if ( $cust_bill_pkg->setup != 0 ) {
2593 'description' => $desc,
2594 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2597 if ( $cust_bill_pkg->recur != 0 ) {
2599 'description' => "$desc (".
2600 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2601 time2str("%x", $cust_bill_pkg->edate). ')',
2602 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2614 sub _items_credits {
2619 foreach ( $self->cust_credited ) {
2621 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2623 my $reason = $_->cust_credit->reason;
2624 #my $reason = substr($_->cust_credit->reason,0,32);
2625 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2626 $reason = " ($reason) " if $reason;
2628 #'description' => 'Credit ref\#'. $_->crednum.
2629 # " (". time2str("%x",$_->cust_credit->_date) .")".
2631 'description' => 'Credit applied '.
2632 time2str("%x",$_->cust_credit->_date). $reason,
2633 'amount' => sprintf("%.2f",$_->amount),
2636 #foreach ( @cr_cust_credit ) {
2638 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2639 # $money_char. sprintf("%10.2f",$_->credited)
2647 sub _items_payments {
2651 #get & print payments
2652 foreach ( $self->cust_bill_pay ) {
2654 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2657 'description' => "Payment received ".
2658 time2str("%x",$_->cust_pay->_date ),
2659 'amount' => sprintf("%.2f", $_->amount )
2678 sub process_reprint {
2679 process_re_X('print', @_);
2686 sub process_reemail {
2687 process_re_X('email', @_);
2695 process_re_X('fax', @_);
2698 use Storable qw(thaw);
2702 my( $method, $job ) = ( shift, shift );
2703 warn "process_re_X $method for job $job\n" if $DEBUG;
2705 my $param = thaw(decode_base64(shift));
2706 warn Dumper($param) if $DEBUG;
2717 my($method, $job, %param ) = @_;
2718 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2720 warn "re_X $method for job $job with param:\n".
2721 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2724 #some false laziness w/search/cust_bill.html
2726 my $orderby = 'ORDER BY cust_bill._date';
2730 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2731 push @where, "cust_bill._date >= $1";
2733 if ( $param{'end'} =~ /^(\d+)$/ ) {
2734 push @where, "cust_bill._date < $1";
2736 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2737 push @where, "cust_main.agentnum = $1";
2741 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2742 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2743 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2744 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2746 push @where, "0 != $owed"
2749 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2752 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2754 my $addl_from = 'left join cust_main using ( custnum )';
2756 if ( $param{'newest_percust'} ) {
2757 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2758 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2759 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2762 my @cust_bill = qsearch( 'cust_bill',
2764 "$distinct cust_bill.*",
2770 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2771 foreach my $cust_bill ( @cust_bill ) {
2772 $cust_bill->$method();
2774 if ( $job ) { #progressbar foo
2776 if ( time - $min_sec > $last ) {
2777 my $error = $job->update_statustext(
2778 int( 100 * $num / scalar(@cust_bill) )
2780 die $error if $error;
2795 print_text formatting (and some logic :/) is in source, but needs to be
2796 slurped in from a file. Also number of lines ($=).
2800 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2801 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base