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\.\-]+)/ or $1 = 'example.com';
575 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
577 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
579 if ( defined($args{'_template'}) && length($args{'_template'})
580 && -e "$path/logo_". $args{'_template'}. ".png"
583 $file = "$path/logo_". $args{'_template'}. ".png";
585 $file = "$path/logo.png";
588 my $image = build MIME::Entity
589 'Type' => 'image/png',
590 'Encoding' => 'base64',
592 'Filename' => 'logo.png',
593 'Content-ID' => "<$content_id>",
596 $alternative->attach(
597 'Type' => 'text/html',
598 'Encoding' => 'quoted-printable',
599 'Data' => [ '<html>',
602 ' '. encode_entities($return{'subject'}),
605 ' <body bgcolor="#e8e8e8">',
606 $self->print_html('', $args{'template'}, $content_id),
610 'Disposition' => 'inline',
611 #'Filename' => 'invoice.pdf',
614 if ( $conf->exists('invoice_email_pdf') ) {
619 # multipart/alternative
625 my $related = build MIME::Entity 'Type' => 'multipart/related',
626 'Encoding' => '7bit';
628 #false laziness w/Misc::send_email
629 $related->head->replace('Content-type',
631 '; boundary="'. $related->head->multipart_boundary. '"'.
632 '; type=multipart/alternative'
635 $related->add_part($alternative);
637 $related->add_part($image);
639 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
641 $return{'mimeparts'} = [ $related, $pdf ];
645 #no other attachment:
647 # multipart/alternative
652 $return{'content-type'} = 'multipart/related';
653 $return{'mimeparts'} = [ $alternative, $image ];
654 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
655 #$return{'disposition'} = 'inline';
661 if ( $conf->exists('invoice_email_pdf') ) {
662 warn "$me creating PDF attachment"
665 #mime parts arguments a la MIME::Entity->build().
666 $return{'mimeparts'} = [
667 { $self->mimebuild_pdf('', $args{'template'}) }
671 if ( $conf->exists('invoice_email_pdf')
672 and scalar($conf->config('invoice_email_pdf_note')) ) {
674 warn "$me using 'invoice_email_pdf_note'"
676 $return{'body'} = [ map { $_ . "\n" }
677 $conf->config('invoice_email_pdf_note')
682 warn "$me not using 'invoice_email_pdf_note'"
684 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
685 $return{'body'} = $args{'print_text'};
687 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
700 Returns a list suitable for passing to MIME::Entity->build(), representing
701 this invoice as PDF attachment.
708 'Type' => 'application/pdf',
709 'Encoding' => 'base64',
710 'Data' => [ $self->print_pdf(@_) ],
711 'Disposition' => 'attachment',
712 'Filename' => 'invoice.pdf',
716 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
718 Sends this invoice to the destinations configured for this customer: sends
719 email, prints and/or faxes. See L<FS::cust_main_invoice>.
721 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
723 AGENTNUM, if specified, means that this invoice will only be sent for customers
724 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
725 single agent) or an arrayref of agentnums.
727 INVOICE_FROM, if specified, overrides the default email invoice From: address.
733 my $template = scalar(@_) ? shift : '';
734 if ( scalar(@_) && $_[0] ) {
735 my $agentnums = ref($_[0]) ? shift : [ shift ];
736 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
742 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
744 my @invoicing_list = $self->cust_main->invoicing_list;
746 $self->email($template, $invoice_from)
747 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
749 $self->print($template)
750 if grep { $_ eq 'POST' } @invoicing_list; #postal
752 $self->fax($template)
753 if grep { $_ eq 'FAX' } @invoicing_list; #fax
759 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
763 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
765 INVOICE_FROM, if specified, overrides the default email invoice From: address.
771 my $template = scalar(@_) ? shift : '';
775 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
777 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
778 $self->cust_main->invoicing_list;
780 #better to notify this person than silence
781 @invoicing_list = ($invoice_from) unless @invoicing_list;
783 my $error = send_email(
784 $self->generate_email(
785 'from' => $invoice_from,
786 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
787 'template' => $template,
790 die "can't email invoice: $error\n" if $error;
791 #die "$error\n" if $error;
795 =item lpr_data [ TEMPLATENAME ]
797 Returns the postscript or plaintext for this invoice as an arrayref.
799 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
804 my( $self, $template) = @_;
805 $conf->exists('invoice_latex')
806 ? [ $self->print_ps('', $template) ]
807 : [ $self->print_text('', $template) ];
810 =item print [ TEMPLATENAME ]
814 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
820 my $template = scalar(@_) ? shift : '';
822 my $lpr = $conf->config('lpr');
825 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
827 $outerr = ": $outerr" if length($outerr);
828 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
833 =item fax [ TEMPLATENAME ]
837 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
843 my $template = scalar(@_) ? shift : '';
845 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
846 unless $conf->exists('invoice_latex');
848 my $dialstring = $self->cust_main->getfield('fax');
851 my $error = send_fax( 'docdata' => $self->lpr_data($template),
852 'dialstring' => $dialstring,
854 die $error if $error;
858 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
860 Like B<send>, but only sends the invoice if it is the newest open invoice for
870 grep { $_->owed > 0 }
871 qsearch('cust_bill', {
872 'custnum' => $self->custnum,
873 #'_date' => { op=>'>', value=>$self->_date },
874 'invnum' => { op=>'>', value=>$self->invnum },
881 =item send_csv OPTION => VALUE, ...
883 Sends invoice as a CSV data-file to a remote host with the specified protocol.
887 protocol - currently only "ftp"
893 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
894 and YYMMDDHHMMSS is a timestamp.
896 See L</print_csv> for a description of the output format.
901 my($self, %opt) = @_;
905 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
906 mkdir $spooldir, 0700 unless -d $spooldir;
908 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
909 my $file = "$spooldir/$tracctnum.csv";
911 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
913 open(CSV, ">$file") or die "can't open $file: $!";
921 if ( $opt{protocol} eq 'ftp' ) {
922 eval "use Net::FTP;";
924 $net = Net::FTP->new($opt{server}) or die @$;
926 die "unknown protocol: $opt{protocol}";
929 $net->login( $opt{username}, $opt{password} )
930 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
932 $net->binary or die "can't set binary mode";
934 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
936 $net->put($file) or die "can't put $file: $!";
946 Spools CSV invoice data.
952 =item format - 'default' or 'billco'
954 =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>).
956 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
958 =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.
965 my($self, %opt) = @_;
967 my $cust_main = $self->cust_main;
969 if ( $opt{'dest'} ) {
970 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
971 $cust_main->invoicing_list;
972 return 'N/A' unless $invoicing_list{$opt{'dest'}}
973 || ! keys %invoicing_list;
976 if ( $opt{'balanceover'} ) {
978 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
981 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
982 mkdir $spooldir, 0700 unless -d $spooldir;
984 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
988 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
989 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
992 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
994 open(CSV, ">>$file") or die "can't open $file: $!";
1000 if ( lc($opt{'format'}) eq 'billco' ) {
1002 flock(CSV, LOCK_UN);
1007 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1010 open(CSV,">>$file") or die "can't open $file: $!";
1011 flock(CSV, LOCK_EX);
1017 flock(CSV, LOCK_UN);
1024 =item print_csv OPTION => VALUE, ...
1026 Returns CSV data for this invoice.
1030 format - 'default' or 'billco'
1032 Returns a list consisting of two scalars. The first is a single line of CSV
1033 header information for this invoice. The second is one or more lines of CSV
1034 detail information for this invoice.
1036 If I<format> is not specified or "default", the fields of the CSV file are as
1039 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1043 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1045 B<record_type> is C<cust_bill> for the initial header line only. The
1046 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1047 fields are filled in.
1049 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1050 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1053 =item invnum - invoice number
1055 =item custnum - customer number
1057 =item _date - invoice date
1059 =item charged - total invoice amount
1061 =item first - customer first name
1063 =item last - customer first name
1065 =item company - company name
1067 =item address1 - address line 1
1069 =item address2 - address line 1
1079 =item pkg - line item description
1081 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1083 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1085 =item sdate - start date for recurring fee
1087 =item edate - end date for recurring fee
1091 If I<format> is "billco", the fields of the header CSV file are as follows:
1093 +-------------------------------------------------------------------+
1094 | FORMAT HEADER FILE |
1095 |-------------------------------------------------------------------|
1096 | Field | Description | Name | Type | Width |
1097 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1098 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1099 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1100 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1101 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1102 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1103 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1104 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1105 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1106 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1107 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1108 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1109 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1110 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1111 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1112 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1113 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1114 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1115 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1116 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1117 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1118 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1119 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1120 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1121 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1122 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1123 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1124 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1125 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1126 +-------+-------------------------------+------------+------+-------+
1128 If I<format> is "billco", the fields of the detail CSV file are as follows:
1130 FORMAT FOR DETAIL FILE
1132 Field | Description | Name | Type | Width
1133 1 | N/A-Leave Empty | RC | CHAR | 2
1134 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1135 3 | Account Number | TRACCTNUM | CHAR | 15
1136 4 | Invoice Number | TRINVOICE | CHAR | 15
1137 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1138 6 | Transaction Detail | DETAILS | CHAR | 100
1139 7 | Amount | AMT | NUM* | 9
1140 8 | Line Format Control** | LNCTRL | CHAR | 2
1141 9 | Grouping Code | GROUP | CHAR | 2
1142 10 | User Defined | ACCT CODE | CHAR | 15
1147 my($self, %opt) = @_;
1149 eval "use Text::CSV_XS";
1152 my $cust_main = $self->cust_main;
1154 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1156 if ( lc($opt{'format'}) eq 'billco' ) {
1159 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1162 if ( $conf->exists('invoice_default_terms')
1163 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1164 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1167 my( $previous_balance, @unused ) = $self->previous; #previous balance
1169 my $pmt_cr_applied = 0;
1170 $pmt_cr_applied += $_->{'amount'}
1171 foreach ( $self->_items_payments, $self->_items_credits ) ;
1173 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1176 '', # 1 | N/A-Leave Empty CHAR 2
1177 '', # 2 | N/A-Leave Empty CHAR 15
1178 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1179 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1180 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1181 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1182 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1183 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1184 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1185 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1186 '', # 10 | Ancillary Billing Information CHAR 30
1187 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1188 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1191 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1194 $duedate, # 14 | Bill Due Date CHAR 10
1196 $previous_balance, # 15 | Previous Balance NUM* 9
1197 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1198 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1199 $totaldue, # 18 | Total Amt Due NUM* 9
1200 $totaldue, # 19 | Total Amt Due NUM* 9
1201 '', # 20 | 30 Day Aging NUM* 9
1202 '', # 21 | 60 Day Aging NUM* 9
1203 '', # 22 | 90 Day Aging NUM* 9
1204 'N', # 23 | Y/N CHAR 1
1205 '', # 24 | Remittance automation CHAR 100
1206 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1207 $self->custnum, # 26 | Customer Reference Number CHAR 15
1208 '0', # 27 | Federal Tax*** NUM* 9
1209 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1210 '0', # 29 | Other Taxes & Fees*** NUM* 9
1219 time2str("%x", $self->_date),
1220 sprintf("%.2f", $self->charged),
1221 ( map { $cust_main->getfield($_) }
1222 qw( first last company address1 address2 city state zip country ) ),
1224 ) or die "can't create csv";
1227 my $header = $csv->string. "\n";
1230 if ( lc($opt{'format'}) eq 'billco' ) {
1233 foreach my $item ( $self->_items_pkg ) {
1236 '', # 1 | N/A-Leave Empty CHAR 2
1237 '', # 2 | N/A-Leave Empty CHAR 15
1238 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1239 $self->invnum, # 4 | Invoice Number CHAR 15
1240 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1241 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1242 $item->{'amount'}, # 7 | Amount NUM* 9
1243 '', # 8 | Line Format Control** CHAR 2
1244 '', # 9 | Grouping Code CHAR 2
1245 '', # 10 | User Defined CHAR 15
1248 $detail .= $csv->string. "\n";
1254 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1256 my($pkg, $setup, $recur, $sdate, $edate);
1257 if ( $cust_bill_pkg->pkgnum ) {
1259 ($pkg, $setup, $recur, $sdate, $edate) = (
1260 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1261 ( $cust_bill_pkg->setup != 0
1262 ? sprintf("%.2f", $cust_bill_pkg->setup )
1264 ( $cust_bill_pkg->recur != 0
1265 ? sprintf("%.2f", $cust_bill_pkg->recur )
1267 ( $cust_bill_pkg->sdate
1268 ? time2str("%x", $cust_bill_pkg->sdate)
1270 ($cust_bill_pkg->edate
1271 ?time2str("%x", $cust_bill_pkg->edate)
1275 } else { #pkgnum tax
1276 next unless $cust_bill_pkg->setup != 0;
1277 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1278 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1280 ($pkg, $setup, $recur, $sdate, $edate) =
1281 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1287 ( map { '' } (1..11) ),
1288 ($pkg, $setup, $recur, $sdate, $edate)
1289 ) or die "can't create csv";
1291 $detail .= $csv->string. "\n";
1297 ( $header, $detail );
1303 Pays this invoice with a compliemntary payment. If there is an error,
1304 returns the error, otherwise returns false.
1310 my $cust_pay = new FS::cust_pay ( {
1311 'invnum' => $self->invnum,
1312 'paid' => $self->owed,
1315 'payinfo' => $self->cust_main->payinfo,
1323 Attempts to pay this invoice with a credit card payment via a
1324 Business::OnlinePayment realtime gateway. See
1325 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1326 for supported processors.
1332 $self->realtime_bop( 'CC', @_ );
1337 Attempts to pay this invoice with an electronic check (ACH) payment via a
1338 Business::OnlinePayment realtime gateway. See
1339 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1340 for supported processors.
1346 $self->realtime_bop( 'ECHECK', @_ );
1351 Attempts to pay this invoice with phone bill (LEC) payment via a
1352 Business::OnlinePayment realtime gateway. See
1353 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1354 for supported processors.
1360 $self->realtime_bop( 'LEC', @_ );
1364 my( $self, $method ) = @_;
1366 my $cust_main = $self->cust_main;
1367 my $balance = $cust_main->balance;
1368 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1369 $amount = sprintf("%.2f", $amount);
1370 return "not run (balance $balance)" unless $amount > 0;
1372 my $description = 'Internet Services';
1373 if ( $conf->exists('business-onlinepayment-description') ) {
1374 my $dtempl = $conf->config('business-onlinepayment-description');
1376 my $agent_obj = $cust_main->agent
1377 or die "can't retreive agent for $cust_main (agentnum ".
1378 $cust_main->agentnum. ")";
1379 my $agent = $agent_obj->agent;
1380 my $pkgs = join(', ',
1381 map { $_->cust_pkg->part_pkg->pkg }
1382 grep { $_->pkgnum } $self->cust_bill_pkg
1384 $description = eval qq("$dtempl");
1387 $cust_main->realtime_bop($method, $amount,
1388 'description' => $description,
1389 'invnum' => $self->invnum,
1394 =item batch_card OPTION => VALUE...
1396 Adds a payment for this invoice to the pending credit card batch (see
1397 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1398 runs the payment using a realtime gateway.
1403 my ($self, %options) = @_;
1404 my $cust_main = $self->cust_main;
1406 my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1407 return '' unless $amount > 0;
1409 if ($options{'realtime'}) {
1410 return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1416 my $oldAutoCommit = $FS::UID::AutoCommit;
1417 local $FS::UID::AutoCommit = 0;
1420 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1421 or return "Cannot lock pay_batch: " . $dbh->errstr;
1425 'payby' => FS::payby->payby2payment($cust_main->payby),
1428 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1430 unless ( $pay_batch ) {
1431 $pay_batch = new FS::pay_batch \%pay_batch;
1432 my $error = $pay_batch->insert;
1434 $dbh->rollback if $oldAutoCommit;
1435 die "error creating new batch: $error\n";
1439 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1440 'batchnum' => $pay_batch->batchnum,
1441 'custnum' => $cust_main->custnum,
1444 my $cust_pay_batch = new FS::cust_pay_batch ( {
1445 'batchnum' => $pay_batch->batchnum,
1446 'invnum' => $self->getfield('invnum'), # is there a better value?
1447 # this field should be
1449 # cust_bill_pay_batch now
1450 'custnum' => $cust_main->custnum,
1451 'last' => $cust_main->getfield('last'),
1452 'first' => $cust_main->getfield('first'),
1453 'address1' => $cust_main->address1,
1454 'address2' => $cust_main->address2,
1455 'city' => $cust_main->city,
1456 'state' => $cust_main->state,
1457 'zip' => $cust_main->zip,
1458 'country' => $cust_main->country,
1459 'payby' => $cust_main->payby,
1460 'payinfo' => $cust_main->payinfo,
1461 'exp' => $cust_main->paydate,
1462 'payname' => $cust_main->payname,
1463 'amount' => $amount, # consolidating
1466 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1467 if $old_cust_pay_batch;
1470 if ($old_cust_pay_batch) {
1471 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1473 $error = $cust_pay_batch->insert;
1477 $dbh->rollback if $oldAutoCommit;
1481 my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1482 foreach my $cust_bill ($cust_main->open_cust_bill) {
1483 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1484 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1485 'invnum' => $cust_bill->invnum,
1486 'paybatchnum' => $cust_pay_batch->paybatchnum,
1487 'amount' => $cust_bill->owed,
1490 if ($unapplied >= $cust_bill_pay_batch->amount){
1491 $unapplied -= $cust_bill_pay_batch->amount;
1494 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
1495 $cust_bill_pay_batch->amount - $unapplied ));
1498 $error = $cust_bill_pay_batch->insert;
1500 $dbh->rollback if $oldAutoCommit;
1505 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1509 sub _agent_template {
1511 $self->_agent_plandata('agent_templatename');
1514 sub _agent_invoice_from {
1516 $self->_agent_plandata('agent_invoice_from');
1519 sub _agent_plandata {
1520 my( $self, $option ) = @_;
1522 my $part_bill_event = qsearchs( 'part_bill_event',
1524 'payby' => $self->cust_main->payby,
1525 'plan' => 'send_agent',
1526 'plandata' => { 'op' => '~',
1527 'value' => "(^|\n)agentnum ".
1529 $self->cust_main->agentnum.
1535 'ORDER BY seconds LIMIT 1'
1538 return '' unless $part_bill_event;
1540 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1543 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1544 " plandata for $option";
1550 =item print_text [ TIME [ , TEMPLATE ] ]
1552 Returns an text invoice, as a list of lines.
1554 TIME an optional value used to control the printing of overdue messages. The
1555 default is now. It isn't the date of the invoice; that's the `_date' field.
1556 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1557 L<Time::Local> and L<Date::Parse> for conversion functions.
1561 #still some false laziness w/_items stuff (and send_csv)
1564 my( $self, $today, $template ) = @_;
1567 # my $invnum = $self->invnum;
1568 my $cust_main = $self->cust_main;
1569 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1570 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1572 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1573 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1574 #my $balance_due = $self->owed + $pr_total - $cr_total;
1575 my $balance_due = $self->owed + $pr_total;
1578 #my($description,$amount);
1582 foreach ( @pr_cust_bill ) {
1584 "Previous Balance, Invoice #". $_->invnum.
1585 " (". time2str("%x",$_->_date). ")",
1586 $money_char. sprintf("%10.2f",$_->owed)
1589 if (@pr_cust_bill) {
1590 push @buf,['','-----------'];
1591 push @buf,[ 'Total Previous Balance',
1592 $money_char. sprintf("%10.2f",$pr_total ) ];
1597 foreach my $cust_bill_pkg (
1598 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1599 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1602 my $desc = $cust_bill_pkg->desc;
1604 if ( $cust_bill_pkg->pkgnum > 0 ) {
1606 if ( $cust_bill_pkg->setup != 0 ) {
1607 my $description = $desc;
1608 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1609 push @buf, [ $description,
1610 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1612 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1613 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1616 if ( $cust_bill_pkg->recur != 0 ) {
1618 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1619 time2str("%x", $cust_bill_pkg->edate) . ")",
1620 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1623 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1624 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1625 $cust_bill_pkg->sdate );
1628 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1630 } else { #pkgnum tax or one-shot line item
1632 if ( $cust_bill_pkg->setup != 0 ) {
1634 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1636 if ( $cust_bill_pkg->recur != 0 ) {
1637 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1638 . time2str("%x", $cust_bill_pkg->edate). ")",
1639 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1647 push @buf,['','-----------'];
1648 push @buf,['Total New Charges',
1649 $money_char. sprintf("%10.2f",$self->charged) ];
1652 push @buf,['','-----------'];
1653 push @buf,['Total Charges',
1654 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1658 foreach ( $self->cust_credited ) {
1660 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1662 my $reason = substr($_->cust_credit->reason,0,32);
1663 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1664 $reason = " ($reason) " if $reason;
1666 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1668 $money_char. sprintf("%10.2f",$_->amount)
1671 #foreach ( @cr_cust_credit ) {
1673 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1674 # $money_char. sprintf("%10.2f",$_->credited)
1678 #get & print payments
1679 foreach ( $self->cust_bill_pay ) {
1681 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1684 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1685 $money_char. sprintf("%10.2f",$_->amount )
1690 my $balance_due_msg = $self->balance_due_msg;
1692 push @buf,['','-----------'];
1693 push @buf,[$balance_due_msg, $money_char.
1694 sprintf("%10.2f", $balance_due ) ];
1696 #create the template
1697 $template ||= $self->_agent_template;
1698 my $templatefile = 'invoice_template';
1699 $templatefile .= "_$template" if length($template);
1700 my @invoice_template = $conf->config($templatefile)
1701 or die "cannot load config file $templatefile";
1704 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1705 /invoice_lines\((\d*)\)/;
1706 $invoice_lines += $1 || scalar(@buf);
1709 die "no invoice_lines() functions in template?" unless $wasfunc;
1710 my $invoice_template = new Text::Template (
1712 SOURCE => [ map "$_\n", @invoice_template ],
1713 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1714 $invoice_template->compile()
1715 or die "can't compile template: $Text::Template::ERROR";
1717 #setup template variables
1718 package FS::cust_bill::_template; #!
1719 use vars qw( $custnum $invnum $date $agent @address $overdue
1720 $page $total_pages @buf );
1722 $custnum = $self->custnum;
1723 $invnum = $self->invnum;
1724 $date = $self->_date;
1725 $agent = $self->cust_main->agent->agent;
1728 if ( $FS::cust_bill::invoice_lines ) {
1730 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1732 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1737 #format address (variable for the template)
1739 @address = ( '', '', '', '', '', '' );
1740 package FS::cust_bill; #!
1741 $FS::cust_bill::_template::address[$l++] =
1742 $cust_main->payname.
1743 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1744 ? " (P.O. #". $cust_main->payinfo. ")"
1748 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1749 if $cust_main->company;
1750 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1751 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1752 if $cust_main->address2;
1753 $FS::cust_bill::_template::address[$l++] =
1754 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1756 my $countrydefault = $conf->config('countrydefault') || 'US';
1757 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1758 unless $cust_main->country eq $countrydefault;
1760 # #overdue? (variable for the template)
1761 # $FS::cust_bill::_template::overdue = (
1763 # && $today > $self->_date
1764 ## && $self->printed > 1
1765 # && $self->printed > 0
1768 #and subroutine for the template
1769 sub FS::cust_bill::_template::invoice_lines {
1770 my $lines = shift || scalar(@buf);
1772 scalar(@buf) ? shift @buf : [ '', '' ];
1778 $FS::cust_bill::_template::page = 1;
1782 push @collect, split("\n",
1783 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1785 $FS::cust_bill::_template::page++;
1788 map "$_\n", @collect;
1792 =item print_latex [ TIME [ , TEMPLATE ] ]
1794 Internal method - returns a filename of a filled-in LaTeX template for this
1795 invoice (Note: add ".tex" to get the actual filename).
1797 See print_ps and print_pdf for methods that return PostScript and PDF output.
1799 TIME an optional value used to control the printing of overdue messages. The
1800 default is now. It isn't the date of the invoice; that's the `_date' field.
1801 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1802 L<Time::Local> and L<Date::Parse> for conversion functions.
1806 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1809 my( $self, $today, $template ) = @_;
1811 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1814 my $cust_main = $self->cust_main;
1815 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1816 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1818 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1819 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1820 #my $balance_due = $self->owed + $pr_total - $cr_total;
1821 my $balance_due = $self->owed + $pr_total;
1823 #create the template
1824 $template ||= $self->_agent_template;
1825 my $templatefile = 'invoice_latex';
1826 my $suffix = length($template) ? "_$template" : '';
1827 $templatefile .= $suffix;
1828 my @invoice_template = map "$_\n", $conf->config($templatefile)
1829 or die "cannot load config file $templatefile";
1831 my($format, $text_template);
1832 if ( grep { /^%%Detail/ } @invoice_template ) {
1833 #change this to a die when the old code is removed
1834 warn "old-style invoice template $templatefile; ".
1835 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1838 $format = 'Text::Template';
1839 $text_template = new Text::Template(
1841 SOURCE => \@invoice_template,
1842 DELIMITERS => [ '[@--', '--@]' ],
1845 $text_template->compile()
1846 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1850 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1851 $returnaddress = join("\n",
1852 $conf->config_orbase('invoice_latexreturnaddress', $template)
1855 $returnaddress = '~';
1858 my %invoice_data = (
1859 'custnum' => $self->custnum,
1860 'invnum' => $self->invnum,
1861 'date' => time2str('%b %o, %Y', $self->_date),
1862 'today' => time2str('%b %o, %Y', $today),
1863 'agent' => _latex_escape($cust_main->agent->agent),
1864 'payname' => _latex_escape($cust_main->payname),
1865 'company' => _latex_escape($cust_main->company),
1866 'address1' => _latex_escape($cust_main->address1),
1867 'address2' => _latex_escape($cust_main->address2),
1868 'city' => _latex_escape($cust_main->city),
1869 'state' => _latex_escape($cust_main->state),
1870 'zip' => _latex_escape($cust_main->zip),
1871 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1872 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1873 'returnaddress' => $returnaddress,
1875 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1876 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1877 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1880 my $countrydefault = $conf->config('countrydefault') || 'US';
1881 if ( $cust_main->country eq $countrydefault ) {
1882 $invoice_data{'country'} = '';
1884 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1887 $invoice_data{'notes'} =
1889 # #do variable substitutions in notes
1890 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1891 $conf->config_orbase('invoice_latexnotes', $template)
1893 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1896 $invoice_data{'footer'} =~ s/\n+$//;
1897 $invoice_data{'smallfooter'} =~ s/\n+$//;
1898 $invoice_data{'notes'} =~ s/\n+$//;
1900 $invoice_data{'po_line'} =
1901 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1902 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1906 if ( $format eq 'old' ) {
1909 my @total_item = ();
1910 while ( @invoice_template ) {
1911 my $line = shift @invoice_template;
1913 if ( $line =~ /^%%Detail\s*$/ ) {
1915 while ( ( my $line_item_line = shift @invoice_template )
1916 !~ /^%%EndDetail\s*$/ ) {
1917 push @line_item, $line_item_line;
1919 foreach my $line_item ( $self->_items ) {
1920 #foreach my $line_item ( $self->_items_pkg ) {
1921 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1922 $invoice_data{'description'} =
1923 _latex_escape($line_item->{'description'});
1924 if ( exists $line_item->{'ext_description'} ) {
1925 $invoice_data{'description'} .=
1926 "\\tabularnewline\n~~".
1927 join( "\\tabularnewline\n~~",
1928 map _latex_escape($_), @{$line_item->{'ext_description'}}
1931 $invoice_data{'amount'} = $line_item->{'amount'};
1932 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1934 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1937 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1939 while ( ( my $total_item_line = shift @invoice_template )
1940 !~ /^%%EndTotalDetails\s*$/ ) {
1941 push @total_item, $total_item_line;
1944 my @total_fill = ();
1947 foreach my $tax ( $self->_items_tax ) {
1948 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1949 $taxtotal += $tax->{'amount'};
1950 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1952 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1957 $invoice_data{'total_item'} = 'Sub-total';
1958 $invoice_data{'total_amount'} =
1959 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1960 unshift @total_fill,
1961 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1965 $invoice_data{'total_item'} = '\textbf{Total}';
1966 $invoice_data{'total_amount'} =
1967 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1969 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1972 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1975 foreach my $credit ( $self->_items_credits ) {
1976 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1978 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1980 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1985 foreach my $payment ( $self->_items_payments ) {
1986 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1988 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1990 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1994 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1995 $invoice_data{'total_amount'} =
1996 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1998 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2001 push @filled_in, @total_fill;
2004 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
2005 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
2006 push @filled_in, $line;
2017 } elsif ( $format eq 'Text::Template' ) {
2019 my @detail_items = ();
2020 my @total_items = ();
2022 $invoice_data{'detail_items'} = \@detail_items;
2023 $invoice_data{'total_items'} = \@total_items;
2025 foreach my $line_item ( $self->_items ) {
2027 ext_description => [],
2029 $detail->{'ref'} = $line_item->{'pkgnum'};
2030 $detail->{'quantity'} = 1;
2031 $detail->{'description'} = _latex_escape($line_item->{'description'});
2032 if ( exists $line_item->{'ext_description'} ) {
2033 @{$detail->{'ext_description'}} = map {
2035 } @{$line_item->{'ext_description'}};
2037 $detail->{'amount'} = $line_item->{'amount'};
2038 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2040 push @detail_items, $detail;
2045 foreach my $tax ( $self->_items_tax ) {
2047 $total->{'total_item'} = _latex_escape($tax->{'description'});
2048 $taxtotal += $tax->{'amount'};
2049 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
2050 push @total_items, $total;
2055 $total->{'total_item'} = 'Sub-total';
2056 $total->{'total_amount'} =
2057 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
2058 unshift @total_items, $total;
2063 $total->{'total_item'} = '\textbf{Total}';
2064 $total->{'total_amount'} =
2065 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
2066 push @total_items, $total;
2069 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2072 foreach my $credit ( $self->_items_credits ) {
2074 $total->{'total_item'} = _latex_escape($credit->{'description'});
2076 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
2077 push @total_items, $total;
2081 foreach my $payment ( $self->_items_payments ) {
2083 $total->{'total_item'} = _latex_escape($payment->{'description'});
2085 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2086 push @total_items, $total;
2091 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2092 $total->{'total_amount'} =
2093 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2094 push @total_items, $total;
2098 die "guru meditation #54";
2101 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2102 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2106 ) or die "can't open temp file: $!\n";
2107 if ( $format eq 'old' ) {
2108 print $fh join('', @filled_in );
2109 } elsif ( $format eq 'Text::Template' ) {
2110 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2112 die "guru meditation #32";
2116 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2121 =item print_ps [ TIME [ , TEMPLATE ] ]
2123 Returns an postscript invoice, as a scalar.
2125 TIME an optional value used to control the printing of overdue messages. The
2126 default is now. It isn't the date of the invoice; that's the `_date' field.
2127 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2128 L<Time::Local> and L<Date::Parse> for conversion functions.
2135 my $file = $self->print_latex(@_);
2137 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2140 my $sfile = shell_quote $file;
2142 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2143 or die "pslatex $file.tex failed; see $file.log for details?\n";
2144 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2145 or die "pslatex $file.tex failed; see $file.log for details?\n";
2147 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2148 or die "dvips failed";
2150 open(POSTSCRIPT, "<$file.ps")
2151 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2153 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2156 while (<POSTSCRIPT>) {
2166 =item print_pdf [ TIME [ , TEMPLATE ] ]
2168 Returns an PDF invoice, as a scalar.
2170 TIME an optional value used to control the printing of overdue messages. The
2171 default is now. It isn't the date of the invoice; that's the `_date' field.
2172 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2173 L<Time::Local> and L<Date::Parse> for conversion functions.
2180 my $file = $self->print_latex(@_);
2182 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2185 #system('pdflatex', "$file.tex");
2186 #system('pdflatex', "$file.tex");
2187 #! LaTeX Error: Unknown graphics extension: .eps.
2189 my $sfile = shell_quote $file;
2191 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2192 or die "pslatex $file.tex failed; see $file.log for details?\n";
2193 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2194 or die "pslatex $file.tex failed; see $file.log for details?\n";
2196 #system('dvipdf', "$file.dvi", "$file.pdf" );
2198 "dvips -q -t letter -f $sfile.dvi ".
2199 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2202 or die "dvips | gs failed: $!";
2204 open(PDF, "<$file.pdf")
2205 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2207 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2220 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2222 Returns an HTML invoice, as a scalar.
2224 TIME an optional value used to control the printing of overdue messages. The
2225 default is now. It isn't the date of the invoice; that's the `_date' field.
2226 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2227 L<Time::Local> and L<Date::Parse> for conversion functions.
2229 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2230 when emailing the invoice as part of a multipart/related MIME email.
2234 #some falze laziness w/print_text and print_latex (and send_csv)
2236 my( $self, $today, $template, $cid ) = @_;
2239 my $cust_main = $self->cust_main;
2240 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2241 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2243 $template ||= $self->_agent_template;
2244 my $templatefile = 'invoice_html';
2245 my $suffix = length($template) ? "_$template" : '';
2246 $templatefile .= $suffix;
2247 my @html_template = map "$_\n", $conf->config($templatefile)
2248 or die "cannot load config file $templatefile";
2250 my $html_template = new Text::Template(
2252 SOURCE => \@html_template,
2253 DELIMITERS => [ '<%=', '%>' ],
2256 $html_template->compile()
2257 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2259 my %invoice_data = (
2260 'custnum' => $self->custnum,
2261 'invnum' => $self->invnum,
2262 'date' => time2str('%b %o, %Y', $self->_date),
2263 'today' => time2str('%b %o, %Y', $today),
2264 'agent' => encode_entities($cust_main->agent->agent),
2265 'payname' => encode_entities($cust_main->payname),
2266 'company' => encode_entities($cust_main->company),
2267 'address1' => encode_entities($cust_main->address1),
2268 'address2' => encode_entities($cust_main->address2),
2269 'city' => encode_entities($cust_main->city),
2270 'state' => encode_entities($cust_main->state),
2271 'zip' => encode_entities($cust_main->zip),
2272 'terms' => $conf->config('invoice_default_terms')
2273 || 'Payable upon receipt',
2275 'template' => $template,
2276 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2280 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2281 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2283 $invoice_data{'returnaddress'} =
2284 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2286 $invoice_data{'returnaddress'} =
2289 s/\\\\\*?\s*$/<BR>/;
2290 s/\\hyphenation\{[\w\s\-]+\}//;
2293 $conf->config_orbase( 'invoice_latexreturnaddress',
2299 my $countrydefault = $conf->config('countrydefault') || 'US';
2300 if ( $cust_main->country eq $countrydefault ) {
2301 $invoice_data{'country'} = '';
2303 $invoice_data{'country'} =
2304 encode_entities(code2country($cust_main->country));
2308 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2309 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2311 $invoice_data{'notes'} =
2312 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2314 $invoice_data{'notes'} =
2316 s/%%(.*)$/<!-- $1 -->/;
2317 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2318 s/\\begin\{enumerate\}/<ol>/;
2320 s/\\end\{enumerate\}/<\/ol>/;
2321 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2324 $conf->config_orbase('invoice_latexnotes', $template)
2328 # #do variable substitutions in notes
2329 # $invoice_data{'notes'} =
2331 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2332 # $conf->config_orbase('invoice_latexnotes', $suffix)
2336 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2337 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2339 $invoice_data{'footer'} =
2340 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2342 $invoice_data{'footer'} =
2343 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2344 $conf->config_orbase('invoice_latexfooter', $template)
2348 $invoice_data{'po_line'} =
2349 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2350 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2353 my $money_char = $conf->config('money_char') || '$';
2355 foreach my $line_item ( $self->_items ) {
2357 ext_description => [],
2359 $detail->{'ref'} = $line_item->{'pkgnum'};
2360 $detail->{'description'} = encode_entities($line_item->{'description'});
2361 if ( exists $line_item->{'ext_description'} ) {
2362 @{$detail->{'ext_description'}} = map {
2363 encode_entities($_);
2364 } @{$line_item->{'ext_description'}};
2366 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2367 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2369 push @{$invoice_data{'detail_items'}}, $detail;
2374 foreach my $tax ( $self->_items_tax ) {
2376 $total->{'total_item'} = encode_entities($tax->{'description'});
2377 $taxtotal += $tax->{'amount'};
2378 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2379 push @{$invoice_data{'total_items'}}, $total;
2384 $total->{'total_item'} = 'Sub-total';
2385 $total->{'total_amount'} =
2386 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2387 unshift @{$invoice_data{'total_items'}}, $total;
2390 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2393 $total->{'total_item'} = '<b>Total</b>';
2394 $total->{'total_amount'} =
2395 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2396 push @{$invoice_data{'total_items'}}, $total;
2399 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2402 foreach my $credit ( $self->_items_credits ) {
2404 $total->{'total_item'} = encode_entities($credit->{'description'});
2406 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2407 push @{$invoice_data{'total_items'}}, $total;
2411 foreach my $payment ( $self->_items_payments ) {
2413 $total->{'total_item'} = encode_entities($payment->{'description'});
2415 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2416 push @{$invoice_data{'total_items'}}, $total;
2421 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2422 $total->{'total_amount'} =
2423 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2424 push @{$invoice_data{'total_items'}}, $total;
2427 $html_template->fill_in( HASH => \%invoice_data);
2430 # quick subroutine for print_latex
2432 # There are ten characters that LaTeX treats as special characters, which
2433 # means that they do not simply typeset themselves:
2434 # # $ % & ~ _ ^ \ { }
2436 # TeX ignores blanks following an escaped character; if you want a blank (as
2437 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2441 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2442 $value =~ s/([<>])/\$$1\$/g;
2446 #utility methods for print_*
2448 sub balance_due_msg {
2450 my $msg = 'Balance Due';
2451 return $msg unless $conf->exists('invoice_default_terms');
2452 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2453 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2454 } elsif ( $conf->config('invoice_default_terms') ) {
2455 $msg .= ' - '. $conf->config('invoice_default_terms');
2462 my @display = scalar(@_)
2464 : qw( _items_previous _items_pkg );
2465 #: qw( _items_pkg );
2466 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2468 foreach my $display ( @display ) {
2469 push @b, $self->$display(@_);
2474 sub _items_previous {
2476 my $cust_main = $self->cust_main;
2477 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2479 foreach ( @pr_cust_bill ) {
2481 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2482 ' ('. time2str('%x',$_->_date). ')',
2483 #'pkgpart' => 'N/A',
2485 'amount' => sprintf("%.2f", $_->owed),
2491 # 'description' => 'Previous Balance',
2492 # #'pkgpart' => 'N/A',
2493 # 'pkgnum' => 'N/A',
2494 # 'amount' => sprintf("%10.2f", $pr_total ),
2495 # 'ext_description' => [ map {
2496 # "Invoice ". $_->invnum.
2497 # " (". time2str("%x",$_->_date). ") ".
2498 # sprintf("%10.2f", $_->owed)
2499 # } @pr_cust_bill ],
2506 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2507 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2512 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2513 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2516 sub _items_cust_bill_pkg {
2518 my $cust_bill_pkg = shift;
2521 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2523 my $desc = $cust_bill_pkg->desc;
2525 if ( $cust_bill_pkg->pkgnum > 0 ) {
2527 if ( $cust_bill_pkg->setup != 0 ) {
2528 my $description = $desc;
2529 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2530 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2531 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2533 description => $description,
2534 #pkgpart => $part_pkg->pkgpart,
2535 pkgnum => $cust_bill_pkg->pkgnum,
2536 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2537 ext_description => \@d,
2541 if ( $cust_bill_pkg->recur != 0 ) {
2543 description => "$desc (" .
2544 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2545 time2str('%x', $cust_bill_pkg->edate). ')',
2546 #pkgpart => $part_pkg->pkgpart,
2547 pkgnum => $cust_bill_pkg->pkgnum,
2548 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2550 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2551 $cust_bill_pkg->sdate),
2552 $cust_bill_pkg->details,
2557 } else { #pkgnum tax or one-shot line item (??)
2559 if ( $cust_bill_pkg->setup != 0 ) {
2561 'description' => $desc,
2562 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2565 if ( $cust_bill_pkg->recur != 0 ) {
2567 'description' => "$desc (".
2568 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2569 time2str("%x", $cust_bill_pkg->edate). ')',
2570 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2582 sub _items_credits {
2587 foreach ( $self->cust_credited ) {
2589 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2591 my $reason = $_->cust_credit->reason;
2592 #my $reason = substr($_->cust_credit->reason,0,32);
2593 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2594 $reason = " ($reason) " if $reason;
2596 #'description' => 'Credit ref\#'. $_->crednum.
2597 # " (". time2str("%x",$_->cust_credit->_date) .")".
2599 'description' => 'Credit applied '.
2600 time2str("%x",$_->cust_credit->_date). $reason,
2601 'amount' => sprintf("%.2f",$_->amount),
2604 #foreach ( @cr_cust_credit ) {
2606 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2607 # $money_char. sprintf("%10.2f",$_->credited)
2615 sub _items_payments {
2619 #get & print payments
2620 foreach ( $self->cust_bill_pay ) {
2622 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2625 'description' => "Payment received ".
2626 time2str("%x",$_->cust_pay->_date ),
2627 'amount' => sprintf("%.2f", $_->amount )
2646 sub process_reprint {
2647 process_re_X('print', @_);
2654 sub process_reemail {
2655 process_re_X('email', @_);
2663 process_re_X('fax', @_);
2666 use Storable qw(thaw);
2670 my( $method, $job ) = ( shift, shift );
2671 warn "process_re_X $method for job $job\n" if $DEBUG;
2673 my $param = thaw(decode_base64(shift));
2674 warn Dumper($param) if $DEBUG;
2685 my($method, $job, %param ) = @_;
2686 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2688 warn "re_X $method for job $job with param:\n".
2689 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2692 #some false laziness w/search/cust_bill.html
2694 my $orderby = 'ORDER BY cust_bill._date';
2698 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2699 push @where, "cust_bill._date >= $1";
2701 if ( $param{'end'} =~ /^(\d+)$/ ) {
2702 push @where, "cust_bill._date < $1";
2704 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2705 push @where, "cust_main.agentnum = $1";
2709 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2710 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2711 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2712 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2714 push @where, "0 != $owed"
2717 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2720 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2722 my $addl_from = 'left join cust_main using ( custnum )';
2724 if ( $param{'newest_percust'} ) {
2725 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2726 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2727 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2730 my @cust_bill = qsearch( 'cust_bill',
2732 "$distinct cust_bill.*",
2738 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2739 foreach my $cust_bill ( @cust_bill ) {
2740 $cust_bill->$method();
2742 if ( $job ) { #progressbar foo
2744 if ( time - $min_sec > $last ) {
2745 my $error = $job->update_statustext(
2746 int( 100 * $num / scalar(@cust_bill) )
2748 die $error if $error;
2763 print_text formatting (and some logic :/) is in source, but needs to be
2764 slurped in from a file. Also number of lines ($=).
2768 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2769 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base