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
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
28 use FS::cust_bill_pay;
29 use FS::cust_bill_pay_batch;
30 use FS::part_bill_event;
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
36 $me = '[FS::cust_bill]';
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub {
41 $money_char = $conf->config('money_char') || '$';
46 FS::cust_bill - Object methods for cust_bill records
52 $record = new FS::cust_bill \%hash;
53 $record = new FS::cust_bill { 'column' => 'value' };
55 $error = $record->insert;
57 $error = $new_record->replace($old_record);
59 $error = $record->delete;
61 $error = $record->check;
63 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
65 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
67 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
69 @cust_pay_objects = $cust_bill->cust_pay;
71 $tax_amount = $record->tax;
73 @lines = $cust_bill->print_text;
74 @lines = $cust_bill->print_text $time;
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
81 following fields are currently supported:
85 =item invnum - primary key (assigned automatically for new invoices)
87 =item custnum - customer (see L<FS::cust_main>)
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
92 =item charged - amount of this invoice
94 =item printed - deprecated
96 =item closed - books closed flag, empty or `Y'
106 Creates a new invoice. To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
112 sub table { 'cust_bill'; }
114 sub cust_linked { $_[0]->cust_main_custnum; }
115 sub cust_unlinked_msg {
117 "WARNING: can't find cust_main.custnum ". $self->custnum.
118 ' (cust_bill.invnum '. $self->invnum. ')';
123 Adds this invoice to the database ("Posts" the invoice). If there is an error,
124 returns the error, otherwise returns false.
128 This method now works but you probably shouldn't use it. Instead, apply a
129 credit against the invoice.
131 Using this method to delete invoices outright is really, really bad. There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
135 Really, don't use it.
141 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
142 $self->SUPER::delete(@_);
145 =item replace OLD_RECORD
147 Replaces the OLD_RECORD with this one in the database. If there is an error,
148 returns the error, otherwise returns false.
150 Only printed may be changed. printed is normally updated by calling the
151 collect method of a customer object (see L<FS::cust_main>).
155 #replace can be inherited from Record.pm
157 # replace_check is now the preferred way to #implement replace data checks
158 # (so $object->replace() works without an argument)
161 my( $new, $old ) = ( shift, shift );
162 return "Can't change custnum!" unless $old->custnum == $new->custnum;
163 #return "Can't change _date!" unless $old->_date eq $new->_date;
164 return "Can't change _date!" unless $old->_date == $new->_date;
165 return "Can't change charged!" unless $old->charged == $new->charged
166 || $old->charged == 0;
173 Checks all fields to make sure this is a valid invoice. If there is an error,
174 returns the error, otherwise returns false. Called by the insert and replace
183 $self->ut_numbern('invnum')
184 || $self->ut_number('custnum')
185 || $self->ut_numbern('_date')
186 || $self->ut_money('charged')
187 || $self->ut_numbern('printed')
188 || $self->ut_enum('closed', [ '', 'Y' ])
190 return $error if $error;
192 return "Unknown customer"
193 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
195 $self->_date(time) unless $self->_date;
197 $self->printed(0) if $self->printed eq '';
204 Returns a list consisting of the total previous balance for this customer,
205 followed by the previous outstanding invoices (as FS::cust_bill objects also).
212 my @cust_bill = sort { $a->_date <=> $b->_date }
213 grep { $_->owed != 0 && $_->_date < $self->_date }
214 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
216 foreach ( @cust_bill ) { $total += $_->owed; }
222 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
228 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
231 =item open_cust_bill_pkg
233 Returns the open line items for this invoice.
235 Note that cust_bill_pkg with both setup and recur fees are returned as two
236 separate line items, each with only one fee.
240 # modeled after cust_main::open_cust_bill
241 sub open_cust_bill_pkg {
244 # grep { $_->owed > 0 } $self->cust_bill_pkg
246 my %other = ( 'recur' => 'setup',
247 'setup' => 'recur', );
249 foreach my $field ( qw( recur setup )) {
250 push @open, map { $_->set( $other{$field}, 0 ); $_; }
251 grep { $_->owed($field) > 0 }
252 $self->cust_bill_pkg;
258 =item cust_bill_event
260 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
265 sub cust_bill_event {
267 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
273 Returns the customer (see L<FS::cust_main>) for this invoice.
279 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
282 =item cust_suspend_if_balance_over AMOUNT
284 Suspends the customer associated with this invoice if the total amount owed on
285 this invoice and all older invoices is greater than the specified amount.
287 Returns a list: an empty list on success or a list of errors.
291 sub cust_suspend_if_balance_over {
292 my( $self, $amount ) = ( shift, shift );
293 my $cust_main = $self->cust_main;
294 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
297 $cust_main->suspend(@_);
303 Depreciated. See the cust_credited method.
305 #Returns a list consisting of the total previous credited (see
306 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
307 #outstanding credits (FS::cust_credit objects).
313 croak "FS::cust_bill->cust_credit depreciated; see ".
314 "FS::cust_bill->cust_credit_bill";
317 #my @cust_credit = sort { $a->_date <=> $b->_date }
318 # grep { $_->credited != 0 && $_->_date < $self->_date }
319 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
321 #foreach (@cust_credit) { $total += $_->credited; }
322 #$total, @cust_credit;
327 Depreciated. See the cust_bill_pay method.
329 #Returns all payments (see L<FS::cust_pay>) for this invoice.
335 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
337 #sort { $a->_date <=> $b->_date }
338 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
344 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
350 sort { $a->_date <=> $b->_date }
351 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
356 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
362 sort { $a->_date <=> $b->_date }
363 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
369 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
376 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
378 foreach (@taxlines) { $total += $_->setup; }
384 Returns the amount owed (still outstanding) on this invoice, which is charged
385 minus all payment applications (see L<FS::cust_bill_pay>) and credit
386 applications (see L<FS::cust_credit_bill>).
392 my $balance = $self->charged;
393 $balance -= $_->amount foreach ( $self->cust_bill_pay );
394 $balance -= $_->amount foreach ( $self->cust_credited );
395 $balance = sprintf( "%.2f", $balance);
396 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
401 =item generate_email PARAMHASH
403 PARAMHASH can contain the following:
407 =item from => sender address, required
409 =item tempate => alternate template name, optional
411 =item print_text => text attachment arrayref, optional
413 =item subject => email subject, optional
417 Returns an argument list to be passed to L<FS::Misc::send_email>.
428 my $me = '[FS::cust_bill::generate_email]';
431 'from' => $args{'from'},
432 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
435 if (ref($args{'to'} eq 'ARRAY')) {
436 $return{'to'} = $args{'to'};
438 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
439 $self->cust_main->invoicing_list
443 if ( $conf->exists('invoice_html') ) {
445 warn "$me creating HTML/text multipart message"
448 $return{'nobody'} = 1;
450 my $alternative = build MIME::Entity
451 'Type' => 'multipart/alternative',
452 'Encoding' => '7bit',
453 'Disposition' => 'inline'
457 if ( $conf->exists('invoice_email_pdf')
458 and scalar($conf->config('invoice_email_pdf_note')) ) {
460 warn "$me using 'invoice_email_pdf_note' in multipart message"
462 $data = [ map { $_ . "\n" }
463 $conf->config('invoice_email_pdf_note')
468 warn "$me not using 'invoice_email_pdf_note' in multipart message"
470 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
471 $data = $args{'print_text'};
473 $data = [ $self->print_text('', $args{'template'}) ];
478 $alternative->attach(
479 'Type' => 'text/plain',
480 #'Encoding' => 'quoted-printable',
481 'Encoding' => '7bit',
483 'Disposition' => 'inline',
486 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
487 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
489 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
491 if ( defined($args{'_template'}) && length($args{'_template'})
492 && -e "$path/logo_". $args{'_template'}. ".png"
495 $file = "$path/logo_". $args{'_template'}. ".png";
497 $file = "$path/logo.png";
500 my $image = build MIME::Entity
501 'Type' => 'image/png',
502 'Encoding' => 'base64',
504 'Filename' => 'logo.png',
505 'Content-ID' => "<$content_id>",
508 $alternative->attach(
509 'Type' => 'text/html',
510 'Encoding' => 'quoted-printable',
511 'Data' => [ '<html>',
514 ' '. encode_entities($return{'subject'}),
517 ' <body bgcolor="#e8e8e8">',
518 $self->print_html('', $args{'template'}, $content_id),
522 'Disposition' => 'inline',
523 #'Filename' => 'invoice.pdf',
526 if ( $conf->exists('invoice_email_pdf') ) {
531 # multipart/alternative
537 my $related = build MIME::Entity 'Type' => 'multipart/related',
538 'Encoding' => '7bit';
540 #false laziness w/Misc::send_email
541 $related->head->replace('Content-type',
543 '; boundary="'. $related->head->multipart_boundary. '"'.
544 '; type=multipart/alternative'
547 $related->add_part($alternative);
549 $related->add_part($image);
551 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
553 $return{'mimeparts'} = [ $related, $pdf ];
557 #no other attachment:
559 # multipart/alternative
564 $return{'content-type'} = 'multipart/related';
565 $return{'mimeparts'} = [ $alternative, $image ];
566 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
567 #$return{'disposition'} = 'inline';
573 if ( $conf->exists('invoice_email_pdf') ) {
574 warn "$me creating PDF attachment"
577 #mime parts arguments a la MIME::Entity->build().
578 $return{'mimeparts'} = [
579 { $self->mimebuild_pdf('', $args{'template'}) }
583 if ( $conf->exists('invoice_email_pdf')
584 and scalar($conf->config('invoice_email_pdf_note')) ) {
586 warn "$me using 'invoice_email_pdf_note'"
588 $return{'body'} = [ map { $_ . "\n" }
589 $conf->config('invoice_email_pdf_note')
594 warn "$me not using 'invoice_email_pdf_note'"
596 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
597 $return{'body'} = $args{'print_text'};
599 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
612 Returns a list suitable for passing to MIME::Entity->build(), representing
613 this invoice as PDF attachment.
620 'Type' => 'application/pdf',
621 'Encoding' => 'base64',
622 'Data' => [ $self->print_pdf(@_) ],
623 'Disposition' => 'attachment',
624 'Filename' => 'invoice.pdf',
628 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
630 Sends this invoice to the destinations configured for this customer: sends
631 email, prints and/or faxes. See L<FS::cust_main_invoice>.
633 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
635 AGENTNUM, if specified, means that this invoice will only be sent for customers
636 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
637 single agent) or an arrayref of agentnums.
639 INVOICE_FROM, if specified, overrides the default email invoice From: address.
645 my $template = scalar(@_) ? shift : '';
646 if ( scalar(@_) && $_[0] ) {
647 my $agentnums = ref($_[0]) ? shift : [ shift ];
648 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
654 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
656 my @invoicing_list = $self->cust_main->invoicing_list;
658 $self->email($template, $invoice_from)
659 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
661 $self->print($template)
662 if grep { $_ eq 'POST' } @invoicing_list; #postal
664 $self->fax($template)
665 if grep { $_ eq 'FAX' } @invoicing_list; #fax
671 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
675 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
677 INVOICE_FROM, if specified, overrides the default email invoice From: address.
683 my $template = scalar(@_) ? shift : '';
687 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
689 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
690 $self->cust_main->invoicing_list;
692 #better to notify this person than silence
693 @invoicing_list = ($invoice_from) unless @invoicing_list;
695 my $error = send_email(
696 $self->generate_email(
697 'from' => $invoice_from,
698 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
699 'template' => $template,
702 die "can't email invoice: $error\n" if $error;
703 #die "$error\n" if $error;
707 =item lpr_data [ TEMPLATENAME ]
709 Returns the postscript or plaintext for this invoice as an arrayref.
711 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
716 my( $self, $template) = @_;
717 $conf->exists('invoice_latex')
718 ? [ $self->print_ps('', $template) ]
719 : [ $self->print_text('', $template) ];
722 =item print [ TEMPLATENAME ]
726 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
732 my $template = scalar(@_) ? shift : '';
734 my $lpr = $conf->config('lpr');
737 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
739 $outerr = ": $outerr" if length($outerr);
740 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
745 =item fax [ TEMPLATENAME ]
749 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
755 my $template = scalar(@_) ? shift : '';
757 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
758 unless $conf->exists('invoice_latex');
760 my $dialstring = $self->cust_main->getfield('fax');
763 my $error = send_fax( 'docdata' => $self->lpr_data($template),
764 'dialstring' => $dialstring,
766 die $error if $error;
770 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
772 Like B<send>, but only sends the invoice if it is the newest open invoice for
782 grep { $_->owed > 0 }
783 qsearch('cust_bill', {
784 'custnum' => $self->custnum,
785 #'_date' => { op=>'>', value=>$self->_date },
786 'invnum' => { op=>'>', value=>$self->invnum },
793 =item send_csv OPTION => VALUE, ...
795 Sends invoice as a CSV data-file to a remote host with the specified protocol.
799 protocol - currently only "ftp"
805 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
806 and YYMMDDHHMMSS is a timestamp.
808 See L</print_csv> for a description of the output format.
813 my($self, %opt) = @_;
817 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
818 mkdir $spooldir, 0700 unless -d $spooldir;
820 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
821 my $file = "$spooldir/$tracctnum.csv";
823 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
825 open(CSV, ">$file") or die "can't open $file: $!";
833 if ( $opt{protocol} eq 'ftp' ) {
834 eval "use Net::FTP;";
836 $net = Net::FTP->new($opt{server}) or die @$;
838 die "unknown protocol: $opt{protocol}";
841 $net->login( $opt{username}, $opt{password} )
842 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
844 $net->binary or die "can't set binary mode";
846 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
848 $net->put($file) or die "can't put $file: $!";
858 Spools CSV invoice data.
864 =item format - 'default' or 'billco'
866 =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>).
868 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
870 =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.
877 my($self, %opt) = @_;
879 my $cust_main = $self->cust_main;
881 if ( $opt{'dest'} ) {
882 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
883 $cust_main->invoicing_list;
884 return 'N/A' unless $invoicing_list{$opt{'dest'}}
885 || ! keys %invoicing_list;
888 if ( $opt{'balanceover'} ) {
890 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
893 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
894 mkdir $spooldir, 0700 unless -d $spooldir;
896 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
900 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
901 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
904 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
906 open(CSV, ">>$file") or die "can't open $file: $!";
912 if ( lc($opt{'format'}) eq 'billco' ) {
919 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
922 open(CSV,">>$file") or die "can't open $file: $!";
936 =item print_csv OPTION => VALUE, ...
938 Returns CSV data for this invoice.
942 format - 'default' or 'billco'
944 Returns a list consisting of two scalars. The first is a single line of CSV
945 header information for this invoice. The second is one or more lines of CSV
946 detail information for this invoice.
948 If I<format> is not specified or "default", the fields of the CSV file are as
951 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
955 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
957 B<record_type> is C<cust_bill> for the initial header line only. The
958 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
959 fields are filled in.
961 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
962 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
965 =item invnum - invoice number
967 =item custnum - customer number
969 =item _date - invoice date
971 =item charged - total invoice amount
973 =item first - customer first name
975 =item last - customer first name
977 =item company - company name
979 =item address1 - address line 1
981 =item address2 - address line 1
991 =item pkg - line item description
993 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
995 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
997 =item sdate - start date for recurring fee
999 =item edate - end date for recurring fee
1003 If I<format> is "billco", the fields of the header CSV file are as follows:
1005 +-------------------------------------------------------------------+
1006 | FORMAT HEADER FILE |
1007 |-------------------------------------------------------------------|
1008 | Field | Description | Name | Type | Width |
1009 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1010 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1011 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1012 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1013 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1014 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1015 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1016 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1017 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1018 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1019 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1020 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1021 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1022 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1023 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1024 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1025 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1026 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1027 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1028 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1029 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1030 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1031 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1032 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1033 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1034 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1035 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1036 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1037 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1038 +-------+-------------------------------+------------+------+-------+
1040 If I<format> is "billco", the fields of the detail CSV file are as follows:
1042 FORMAT FOR DETAIL FILE
1044 Field | Description | Name | Type | Width
1045 1 | N/A-Leave Empty | RC | CHAR | 2
1046 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1047 3 | Account Number | TRACCTNUM | CHAR | 15
1048 4 | Invoice Number | TRINVOICE | CHAR | 15
1049 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1050 6 | Transaction Detail | DETAILS | CHAR | 100
1051 7 | Amount | AMT | NUM* | 9
1052 8 | Line Format Control** | LNCTRL | CHAR | 2
1053 9 | Grouping Code | GROUP | CHAR | 2
1054 10 | User Defined | ACCT CODE | CHAR | 15
1059 my($self, %opt) = @_;
1061 eval "use Text::CSV_XS";
1064 my $cust_main = $self->cust_main;
1066 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1068 if ( lc($opt{'format'}) eq 'billco' ) {
1071 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1074 if ( $conf->exists('invoice_default_terms')
1075 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1076 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1079 my( $previous_balance, @unused ) = $self->previous; #previous balance
1081 my $pmt_cr_applied = 0;
1082 $pmt_cr_applied += $_->{'amount'}
1083 foreach ( $self->_items_payments, $self->_items_credits ) ;
1085 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1088 '', # 1 | N/A-Leave Empty CHAR 2
1089 '', # 2 | N/A-Leave Empty CHAR 15
1090 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1091 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1092 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1093 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1094 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1095 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1096 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1097 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1098 '', # 10 | Ancillary Billing Information CHAR 30
1099 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1100 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1103 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1106 $duedate, # 14 | Bill Due Date CHAR 10
1108 $previous_balance, # 15 | Previous Balance NUM* 9
1109 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1110 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1111 $totaldue, # 18 | Total Amt Due NUM* 9
1112 $totaldue, # 19 | Total Amt Due NUM* 9
1113 '', # 20 | 30 Day Aging NUM* 9
1114 '', # 21 | 60 Day Aging NUM* 9
1115 '', # 22 | 90 Day Aging NUM* 9
1116 'N', # 23 | Y/N CHAR 1
1117 '', # 24 | Remittance automation CHAR 100
1118 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1119 $self->custnum, # 26 | Customer Reference Number CHAR 15
1120 '0', # 27 | Federal Tax*** NUM* 9
1121 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1122 '0', # 29 | Other Taxes & Fees*** NUM* 9
1131 time2str("%x", $self->_date),
1132 sprintf("%.2f", $self->charged),
1133 ( map { $cust_main->getfield($_) }
1134 qw( first last company address1 address2 city state zip country ) ),
1136 ) or die "can't create csv";
1139 my $header = $csv->string. "\n";
1142 if ( lc($opt{'format'}) eq 'billco' ) {
1145 foreach my $item ( $self->_items_pkg ) {
1148 '', # 1 | N/A-Leave Empty CHAR 2
1149 '', # 2 | N/A-Leave Empty CHAR 15
1150 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1151 $self->invnum, # 4 | Invoice Number CHAR 15
1152 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1153 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1154 $item->{'amount'}, # 7 | Amount NUM* 9
1155 '', # 8 | Line Format Control** CHAR 2
1156 '', # 9 | Grouping Code CHAR 2
1157 '', # 10 | User Defined CHAR 15
1160 $detail .= $csv->string. "\n";
1166 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1168 my($pkg, $setup, $recur, $sdate, $edate);
1169 if ( $cust_bill_pkg->pkgnum ) {
1171 ($pkg, $setup, $recur, $sdate, $edate) = (
1172 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1173 ( $cust_bill_pkg->setup != 0
1174 ? sprintf("%.2f", $cust_bill_pkg->setup )
1176 ( $cust_bill_pkg->recur != 0
1177 ? sprintf("%.2f", $cust_bill_pkg->recur )
1179 ( $cust_bill_pkg->sdate
1180 ? time2str("%x", $cust_bill_pkg->sdate)
1182 ($cust_bill_pkg->edate
1183 ?time2str("%x", $cust_bill_pkg->edate)
1187 } else { #pkgnum tax
1188 next unless $cust_bill_pkg->setup != 0;
1189 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1190 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1192 ($pkg, $setup, $recur, $sdate, $edate) =
1193 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1199 ( map { '' } (1..11) ),
1200 ($pkg, $setup, $recur, $sdate, $edate)
1201 ) or die "can't create csv";
1203 $detail .= $csv->string. "\n";
1209 ( $header, $detail );
1215 Pays this invoice with a compliemntary payment. If there is an error,
1216 returns the error, otherwise returns false.
1222 my $cust_pay = new FS::cust_pay ( {
1223 'invnum' => $self->invnum,
1224 'paid' => $self->owed,
1227 'payinfo' => $self->cust_main->payinfo,
1235 Attempts to pay this invoice with a credit card payment via a
1236 Business::OnlinePayment realtime gateway. See
1237 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1238 for supported processors.
1244 $self->realtime_bop( 'CC', @_ );
1249 Attempts to pay this invoice with an electronic check (ACH) payment via a
1250 Business::OnlinePayment realtime gateway. See
1251 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1252 for supported processors.
1258 $self->realtime_bop( 'ECHECK', @_ );
1263 Attempts to pay this invoice with phone bill (LEC) payment via a
1264 Business::OnlinePayment realtime gateway. See
1265 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1266 for supported processors.
1272 $self->realtime_bop( 'LEC', @_ );
1276 my( $self, $method ) = @_;
1278 my $cust_main = $self->cust_main;
1279 my $balance = $cust_main->balance;
1280 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1281 $amount = sprintf("%.2f", $amount);
1282 return "not run (balance $balance)" unless $amount > 0;
1284 my $description = 'Internet Services';
1285 if ( $conf->exists('business-onlinepayment-description') ) {
1286 my $dtempl = $conf->config('business-onlinepayment-description');
1288 my $agent_obj = $cust_main->agent
1289 or die "can't retreive agent for $cust_main (agentnum ".
1290 $cust_main->agentnum. ")";
1291 my $agent = $agent_obj->agent;
1292 my $pkgs = join(', ',
1293 map { $_->cust_pkg->part_pkg->pkg }
1294 grep { $_->pkgnum } $self->cust_bill_pkg
1296 $description = eval qq("$dtempl");
1299 $cust_main->realtime_bop($method, $amount,
1300 'description' => $description,
1301 'invnum' => $self->invnum,
1306 =item batch_card OPTION => VALUE...
1308 Adds a payment for this invoice to the pending credit card batch (see
1309 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1310 runs the payment using a realtime gateway.
1315 my ($self, %options) = @_;
1316 my $cust_main = $self->cust_main;
1318 my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1319 return '' unless $amount > 0;
1321 if ($options{'realtime'}) {
1322 return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1328 my $oldAutoCommit = $FS::UID::AutoCommit;
1329 local $FS::UID::AutoCommit = 0;
1332 $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1333 or return "Cannot lock pay_batch: " . $dbh->errstr;
1337 'payby' => FS::payby->payby2payment($cust_main->payby),
1340 my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1342 unless ( $pay_batch ) {
1343 $pay_batch = new FS::pay_batch \%pay_batch;
1344 my $error = $pay_batch->insert;
1346 $dbh->rollback if $oldAutoCommit;
1347 die "error creating new batch: $error\n";
1351 my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1352 'batchnum' => $pay_batch->batchnum,
1353 'custnum' => $cust_main->custnum,
1356 my $cust_pay_batch = new FS::cust_pay_batch ( {
1357 'batchnum' => $pay_batch->batchnum,
1358 'invnum' => $self->getfield('invnum'), # is there a better value?
1359 # this field should be
1361 # cust_bill_pay_batch now
1362 'custnum' => $cust_main->custnum,
1363 'last' => $cust_main->getfield('last'),
1364 'first' => $cust_main->getfield('first'),
1365 'address1' => $cust_main->address1,
1366 'address2' => $cust_main->address2,
1367 'city' => $cust_main->city,
1368 'state' => $cust_main->state,
1369 'zip' => $cust_main->zip,
1370 'country' => $cust_main->country,
1371 'payby' => $cust_main->payby,
1372 'payinfo' => $cust_main->payinfo,
1373 'exp' => $cust_main->paydate,
1374 'payname' => $cust_main->payname,
1375 'amount' => $amount, # consolidating
1378 $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1379 if $old_cust_pay_batch;
1382 if ($old_cust_pay_batch) {
1383 $error = $cust_pay_batch->replace($old_cust_pay_batch)
1385 $error = $cust_pay_batch->insert;
1389 $dbh->rollback if $oldAutoCommit;
1393 my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1394 foreach my $cust_bill ($cust_main->open_cust_bill) {
1395 #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1396 my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1397 'invnum' => $cust_bill->invnum,
1398 'paybatchnum' => $cust_pay_batch->paybatchnum,
1399 'amount' => $cust_bill->owed,
1402 if ($unapplied >= $cust_bill_pay_batch->amount){
1403 $unapplied -= $cust_bill_pay_batch->amount;
1406 $cust_bill_pay_batch->amount(sprintf ( "%.2f",
1407 $cust_bill_pay_batch->amount - $unapplied ));
1410 $error = $cust_bill_pay_batch->insert;
1412 $dbh->rollback if $oldAutoCommit;
1417 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1421 sub _agent_template {
1423 $self->_agent_plandata('agent_templatename');
1426 sub _agent_invoice_from {
1428 $self->_agent_plandata('agent_invoice_from');
1431 sub _agent_plandata {
1432 my( $self, $option ) = @_;
1434 my $part_bill_event = qsearchs( 'part_bill_event',
1436 'payby' => $self->cust_main->payby,
1437 'plan' => 'send_agent',
1438 'plandata' => { 'op' => '~',
1439 'value' => "(^|\n)agentnum ".
1441 $self->cust_main->agentnum.
1447 'ORDER BY seconds LIMIT 1'
1450 return '' unless $part_bill_event;
1452 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1455 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1456 " plandata for $option";
1462 =item print_text [ TIME [ , TEMPLATE ] ]
1464 Returns an text invoice, as a list of lines.
1466 TIME an optional value used to control the printing of overdue messages. The
1467 default is now. It isn't the date of the invoice; that's the `_date' field.
1468 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1469 L<Time::Local> and L<Date::Parse> for conversion functions.
1473 #still some false laziness w/_items stuff (and send_csv)
1476 my( $self, $today, $template ) = @_;
1479 # my $invnum = $self->invnum;
1480 my $cust_main = $self->cust_main;
1481 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1482 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1484 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1485 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1486 #my $balance_due = $self->owed + $pr_total - $cr_total;
1487 my $balance_due = $self->owed + $pr_total;
1490 #my($description,$amount);
1494 foreach ( @pr_cust_bill ) {
1496 "Previous Balance, Invoice #". $_->invnum.
1497 " (". time2str("%x",$_->_date). ")",
1498 $money_char. sprintf("%10.2f",$_->owed)
1501 if (@pr_cust_bill) {
1502 push @buf,['','-----------'];
1503 push @buf,[ 'Total Previous Balance',
1504 $money_char. sprintf("%10.2f",$pr_total ) ];
1509 foreach my $cust_bill_pkg (
1510 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1511 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1514 my $desc = $cust_bill_pkg->desc;
1516 if ( $cust_bill_pkg->pkgnum > 0 ) {
1518 if ( $cust_bill_pkg->setup != 0 ) {
1519 my $description = $desc;
1520 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1521 push @buf, [ $description,
1522 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1524 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1525 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1528 if ( $cust_bill_pkg->recur != 0 ) {
1530 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1531 time2str("%x", $cust_bill_pkg->edate) . ")",
1532 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1535 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1536 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1537 $cust_bill_pkg->sdate );
1540 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1542 } else { #pkgnum tax or one-shot line item
1544 if ( $cust_bill_pkg->setup != 0 ) {
1546 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1548 if ( $cust_bill_pkg->recur != 0 ) {
1549 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1550 . time2str("%x", $cust_bill_pkg->edate). ")",
1551 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1559 push @buf,['','-----------'];
1560 push @buf,['Total New Charges',
1561 $money_char. sprintf("%10.2f",$self->charged) ];
1564 push @buf,['','-----------'];
1565 push @buf,['Total Charges',
1566 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1570 foreach ( $self->cust_credited ) {
1572 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1574 my $reason = substr($_->cust_credit->reason,0,32);
1575 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1576 $reason = " ($reason) " if $reason;
1578 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1580 $money_char. sprintf("%10.2f",$_->amount)
1583 #foreach ( @cr_cust_credit ) {
1585 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1586 # $money_char. sprintf("%10.2f",$_->credited)
1590 #get & print payments
1591 foreach ( $self->cust_bill_pay ) {
1593 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1596 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1597 $money_char. sprintf("%10.2f",$_->amount )
1602 my $balance_due_msg = $self->balance_due_msg;
1604 push @buf,['','-----------'];
1605 push @buf,[$balance_due_msg, $money_char.
1606 sprintf("%10.2f", $balance_due ) ];
1608 #create the template
1609 $template ||= $self->_agent_template;
1610 my $templatefile = 'invoice_template';
1611 $templatefile .= "_$template" if length($template);
1612 my @invoice_template = $conf->config($templatefile)
1613 or die "cannot load config file $templatefile";
1616 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1617 /invoice_lines\((\d*)\)/;
1618 $invoice_lines += $1 || scalar(@buf);
1621 die "no invoice_lines() functions in template?" unless $wasfunc;
1622 my $invoice_template = new Text::Template (
1624 SOURCE => [ map "$_\n", @invoice_template ],
1625 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1626 $invoice_template->compile()
1627 or die "can't compile template: $Text::Template::ERROR";
1629 #setup template variables
1630 package FS::cust_bill::_template; #!
1631 use vars qw( $custnum $invnum $date $agent @address $overdue
1632 $page $total_pages @buf );
1634 $custnum = $self->custnum;
1635 $invnum = $self->invnum;
1636 $date = $self->_date;
1637 $agent = $self->cust_main->agent->agent;
1640 if ( $FS::cust_bill::invoice_lines ) {
1642 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1644 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1649 #format address (variable for the template)
1651 @address = ( '', '', '', '', '', '' );
1652 package FS::cust_bill; #!
1653 $FS::cust_bill::_template::address[$l++] =
1654 $cust_main->payname.
1655 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1656 ? " (P.O. #". $cust_main->payinfo. ")"
1660 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1661 if $cust_main->company;
1662 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1663 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1664 if $cust_main->address2;
1665 $FS::cust_bill::_template::address[$l++] =
1666 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1668 my $countrydefault = $conf->config('countrydefault') || 'US';
1669 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1670 unless $cust_main->country eq $countrydefault;
1672 # #overdue? (variable for the template)
1673 # $FS::cust_bill::_template::overdue = (
1675 # && $today > $self->_date
1676 ## && $self->printed > 1
1677 # && $self->printed > 0
1680 #and subroutine for the template
1681 sub FS::cust_bill::_template::invoice_lines {
1682 my $lines = shift || scalar(@buf);
1684 scalar(@buf) ? shift @buf : [ '', '' ];
1690 $FS::cust_bill::_template::page = 1;
1694 push @collect, split("\n",
1695 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1697 $FS::cust_bill::_template::page++;
1700 map "$_\n", @collect;
1704 =item print_latex [ TIME [ , TEMPLATE ] ]
1706 Internal method - returns a filename of a filled-in LaTeX template for this
1707 invoice (Note: add ".tex" to get the actual filename).
1709 See print_ps and print_pdf for methods that return PostScript and PDF output.
1711 TIME an optional value used to control the printing of overdue messages. The
1712 default is now. It isn't the date of the invoice; that's the `_date' field.
1713 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1714 L<Time::Local> and L<Date::Parse> for conversion functions.
1718 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1721 my( $self, $today, $template ) = @_;
1723 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1726 my $cust_main = $self->cust_main;
1727 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1728 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1730 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1731 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1732 #my $balance_due = $self->owed + $pr_total - $cr_total;
1733 my $balance_due = $self->owed + $pr_total;
1735 #create the template
1736 $template ||= $self->_agent_template;
1737 my $templatefile = 'invoice_latex';
1738 my $suffix = length($template) ? "_$template" : '';
1739 $templatefile .= $suffix;
1740 my @invoice_template = map "$_\n", $conf->config($templatefile)
1741 or die "cannot load config file $templatefile";
1743 my($format, $text_template);
1744 if ( grep { /^%%Detail/ } @invoice_template ) {
1745 #change this to a die when the old code is removed
1746 warn "old-style invoice template $templatefile; ".
1747 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1750 $format = 'Text::Template';
1751 $text_template = new Text::Template(
1753 SOURCE => \@invoice_template,
1754 DELIMITERS => [ '[@--', '--@]' ],
1757 $text_template->compile()
1758 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1762 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1763 $returnaddress = join("\n",
1764 $conf->config_orbase('invoice_latexreturnaddress', $template)
1767 $returnaddress = '~';
1770 my %invoice_data = (
1771 'custnum' => $self->custnum,
1772 'invnum' => $self->invnum,
1773 'date' => time2str('%b %o, %Y', $self->_date),
1774 'today' => time2str('%b %o, %Y', $today),
1775 'agent' => _latex_escape($cust_main->agent->agent),
1776 'payname' => _latex_escape($cust_main->payname),
1777 'company' => _latex_escape($cust_main->company),
1778 'address1' => _latex_escape($cust_main->address1),
1779 'address2' => _latex_escape($cust_main->address2),
1780 'city' => _latex_escape($cust_main->city),
1781 'state' => _latex_escape($cust_main->state),
1782 'zip' => _latex_escape($cust_main->zip),
1783 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1784 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1785 'returnaddress' => $returnaddress,
1787 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1788 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1789 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1792 my $countrydefault = $conf->config('countrydefault') || 'US';
1793 if ( $cust_main->country eq $countrydefault ) {
1794 $invoice_data{'country'} = '';
1796 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1799 $invoice_data{'notes'} =
1801 # #do variable substitutions in notes
1802 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1803 $conf->config_orbase('invoice_latexnotes', $template)
1805 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1808 $invoice_data{'footer'} =~ s/\n+$//;
1809 $invoice_data{'smallfooter'} =~ s/\n+$//;
1810 $invoice_data{'notes'} =~ s/\n+$//;
1812 $invoice_data{'po_line'} =
1813 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1814 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1818 if ( $format eq 'old' ) {
1821 my @total_item = ();
1822 while ( @invoice_template ) {
1823 my $line = shift @invoice_template;
1825 if ( $line =~ /^%%Detail\s*$/ ) {
1827 while ( ( my $line_item_line = shift @invoice_template )
1828 !~ /^%%EndDetail\s*$/ ) {
1829 push @line_item, $line_item_line;
1831 foreach my $line_item ( $self->_items ) {
1832 #foreach my $line_item ( $self->_items_pkg ) {
1833 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1834 $invoice_data{'description'} =
1835 _latex_escape($line_item->{'description'});
1836 if ( exists $line_item->{'ext_description'} ) {
1837 $invoice_data{'description'} .=
1838 "\\tabularnewline\n~~".
1839 join( "\\tabularnewline\n~~",
1840 map _latex_escape($_), @{$line_item->{'ext_description'}}
1843 $invoice_data{'amount'} = $line_item->{'amount'};
1844 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1846 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1849 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1851 while ( ( my $total_item_line = shift @invoice_template )
1852 !~ /^%%EndTotalDetails\s*$/ ) {
1853 push @total_item, $total_item_line;
1856 my @total_fill = ();
1859 foreach my $tax ( $self->_items_tax ) {
1860 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1861 $taxtotal += $tax->{'amount'};
1862 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1864 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1869 $invoice_data{'total_item'} = 'Sub-total';
1870 $invoice_data{'total_amount'} =
1871 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1872 unshift @total_fill,
1873 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1877 $invoice_data{'total_item'} = '\textbf{Total}';
1878 $invoice_data{'total_amount'} =
1879 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1881 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1884 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1887 foreach my $credit ( $self->_items_credits ) {
1888 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1890 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1892 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1897 foreach my $payment ( $self->_items_payments ) {
1898 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1900 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1902 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1906 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1907 $invoice_data{'total_amount'} =
1908 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1910 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1913 push @filled_in, @total_fill;
1916 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1917 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1918 push @filled_in, $line;
1929 } elsif ( $format eq 'Text::Template' ) {
1931 my @detail_items = ();
1932 my @total_items = ();
1934 $invoice_data{'detail_items'} = \@detail_items;
1935 $invoice_data{'total_items'} = \@total_items;
1937 foreach my $line_item ( $self->_items ) {
1939 ext_description => [],
1941 $detail->{'ref'} = $line_item->{'pkgnum'};
1942 $detail->{'quantity'} = 1;
1943 $detail->{'description'} = _latex_escape($line_item->{'description'});
1944 if ( exists $line_item->{'ext_description'} ) {
1945 @{$detail->{'ext_description'}} = map {
1947 } @{$line_item->{'ext_description'}};
1949 $detail->{'amount'} = $line_item->{'amount'};
1950 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1952 push @detail_items, $detail;
1957 foreach my $tax ( $self->_items_tax ) {
1959 $total->{'total_item'} = _latex_escape($tax->{'description'});
1960 $taxtotal += $tax->{'amount'};
1961 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1962 push @total_items, $total;
1967 $total->{'total_item'} = 'Sub-total';
1968 $total->{'total_amount'} =
1969 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1970 unshift @total_items, $total;
1975 $total->{'total_item'} = '\textbf{Total}';
1976 $total->{'total_amount'} =
1977 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1978 push @total_items, $total;
1981 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1984 foreach my $credit ( $self->_items_credits ) {
1986 $total->{'total_item'} = _latex_escape($credit->{'description'});
1988 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1989 push @total_items, $total;
1993 foreach my $payment ( $self->_items_payments ) {
1995 $total->{'total_item'} = _latex_escape($payment->{'description'});
1997 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1998 push @total_items, $total;
2003 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2004 $total->{'total_amount'} =
2005 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2006 push @total_items, $total;
2010 die "guru meditation #54";
2013 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2014 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2018 ) or die "can't open temp file: $!\n";
2019 if ( $format eq 'old' ) {
2020 print $fh join('', @filled_in );
2021 } elsif ( $format eq 'Text::Template' ) {
2022 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2024 die "guru meditation #32";
2028 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2033 =item print_ps [ TIME [ , TEMPLATE ] ]
2035 Returns an postscript invoice, as a scalar.
2037 TIME an optional value used to control the printing of overdue messages. The
2038 default is now. It isn't the date of the invoice; that's the `_date' field.
2039 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2040 L<Time::Local> and L<Date::Parse> for conversion functions.
2047 my $file = $self->print_latex(@_);
2049 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2052 my $sfile = shell_quote $file;
2054 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2055 or die "pslatex $file.tex failed; see $file.log for details?\n";
2056 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2057 or die "pslatex $file.tex failed; see $file.log for details?\n";
2059 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2060 or die "dvips failed";
2062 open(POSTSCRIPT, "<$file.ps")
2063 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2065 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2068 while (<POSTSCRIPT>) {
2078 =item print_pdf [ TIME [ , TEMPLATE ] ]
2080 Returns an PDF invoice, as a scalar.
2082 TIME an optional value used to control the printing of overdue messages. The
2083 default is now. It isn't the date of the invoice; that's the `_date' field.
2084 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2085 L<Time::Local> and L<Date::Parse> for conversion functions.
2092 my $file = $self->print_latex(@_);
2094 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2097 #system('pdflatex', "$file.tex");
2098 #system('pdflatex', "$file.tex");
2099 #! LaTeX Error: Unknown graphics extension: .eps.
2101 my $sfile = shell_quote $file;
2103 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2104 or die "pslatex $file.tex failed; see $file.log for details?\n";
2105 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2106 or die "pslatex $file.tex failed; see $file.log for details?\n";
2108 #system('dvipdf', "$file.dvi", "$file.pdf" );
2110 "dvips -q -t letter -f $sfile.dvi ".
2111 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2114 or die "dvips | gs failed: $!";
2116 open(PDF, "<$file.pdf")
2117 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2119 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2132 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2134 Returns an HTML invoice, as a scalar.
2136 TIME an optional value used to control the printing of overdue messages. The
2137 default is now. It isn't the date of the invoice; that's the `_date' field.
2138 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2139 L<Time::Local> and L<Date::Parse> for conversion functions.
2141 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2142 when emailing the invoice as part of a multipart/related MIME email.
2146 #some falze laziness w/print_text and print_latex (and send_csv)
2148 my( $self, $today, $template, $cid ) = @_;
2151 my $cust_main = $self->cust_main;
2152 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2153 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2155 $template ||= $self->_agent_template;
2156 my $templatefile = 'invoice_html';
2157 my $suffix = length($template) ? "_$template" : '';
2158 $templatefile .= $suffix;
2159 my @html_template = map "$_\n", $conf->config($templatefile)
2160 or die "cannot load config file $templatefile";
2162 my $html_template = new Text::Template(
2164 SOURCE => \@html_template,
2165 DELIMITERS => [ '<%=', '%>' ],
2168 $html_template->compile()
2169 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2171 my %invoice_data = (
2172 'custnum' => $self->custnum,
2173 'invnum' => $self->invnum,
2174 'date' => time2str('%b %o, %Y', $self->_date),
2175 'today' => time2str('%b %o, %Y', $today),
2176 'agent' => encode_entities($cust_main->agent->agent),
2177 'payname' => encode_entities($cust_main->payname),
2178 'company' => encode_entities($cust_main->company),
2179 'address1' => encode_entities($cust_main->address1),
2180 'address2' => encode_entities($cust_main->address2),
2181 'city' => encode_entities($cust_main->city),
2182 'state' => encode_entities($cust_main->state),
2183 'zip' => encode_entities($cust_main->zip),
2184 'terms' => $conf->config('invoice_default_terms')
2185 || 'Payable upon receipt',
2187 'template' => $template,
2188 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2192 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2193 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2195 $invoice_data{'returnaddress'} =
2196 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2198 $invoice_data{'returnaddress'} =
2201 s/\\\\\*?\s*$/<BR>/;
2202 s/\\hyphenation\{[\w\s\-]+\}//;
2205 $conf->config_orbase( 'invoice_latexreturnaddress',
2211 my $countrydefault = $conf->config('countrydefault') || 'US';
2212 if ( $cust_main->country eq $countrydefault ) {
2213 $invoice_data{'country'} = '';
2215 $invoice_data{'country'} =
2216 encode_entities(code2country($cust_main->country));
2220 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2221 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2223 $invoice_data{'notes'} =
2224 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2226 $invoice_data{'notes'} =
2228 s/%%(.*)$/<!-- $1 -->/;
2229 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2230 s/\\begin\{enumerate\}/<ol>/;
2232 s/\\end\{enumerate\}/<\/ol>/;
2233 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2236 $conf->config_orbase('invoice_latexnotes', $template)
2240 # #do variable substitutions in notes
2241 # $invoice_data{'notes'} =
2243 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2244 # $conf->config_orbase('invoice_latexnotes', $suffix)
2248 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2249 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2251 $invoice_data{'footer'} =
2252 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2254 $invoice_data{'footer'} =
2255 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2256 $conf->config_orbase('invoice_latexfooter', $template)
2260 $invoice_data{'po_line'} =
2261 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2262 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2265 my $money_char = $conf->config('money_char') || '$';
2267 foreach my $line_item ( $self->_items ) {
2269 ext_description => [],
2271 $detail->{'ref'} = $line_item->{'pkgnum'};
2272 $detail->{'description'} = encode_entities($line_item->{'description'});
2273 if ( exists $line_item->{'ext_description'} ) {
2274 @{$detail->{'ext_description'}} = map {
2275 encode_entities($_);
2276 } @{$line_item->{'ext_description'}};
2278 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2279 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2281 push @{$invoice_data{'detail_items'}}, $detail;
2286 foreach my $tax ( $self->_items_tax ) {
2288 $total->{'total_item'} = encode_entities($tax->{'description'});
2289 $taxtotal += $tax->{'amount'};
2290 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2291 push @{$invoice_data{'total_items'}}, $total;
2296 $total->{'total_item'} = 'Sub-total';
2297 $total->{'total_amount'} =
2298 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2299 unshift @{$invoice_data{'total_items'}}, $total;
2302 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2305 $total->{'total_item'} = '<b>Total</b>';
2306 $total->{'total_amount'} =
2307 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2308 push @{$invoice_data{'total_items'}}, $total;
2311 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2314 foreach my $credit ( $self->_items_credits ) {
2316 $total->{'total_item'} = encode_entities($credit->{'description'});
2318 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2319 push @{$invoice_data{'total_items'}}, $total;
2323 foreach my $payment ( $self->_items_payments ) {
2325 $total->{'total_item'} = encode_entities($payment->{'description'});
2327 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2328 push @{$invoice_data{'total_items'}}, $total;
2333 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2334 $total->{'total_amount'} =
2335 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2336 push @{$invoice_data{'total_items'}}, $total;
2339 $html_template->fill_in( HASH => \%invoice_data);
2342 # quick subroutine for print_latex
2344 # There are ten characters that LaTeX treats as special characters, which
2345 # means that they do not simply typeset themselves:
2346 # # $ % & ~ _ ^ \ { }
2348 # TeX ignores blanks following an escaped character; if you want a blank (as
2349 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2353 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2354 $value =~ s/([<>])/\$$1\$/g;
2358 #utility methods for print_*
2360 sub balance_due_msg {
2362 my $msg = 'Balance Due';
2363 return $msg unless $conf->exists('invoice_default_terms');
2364 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2365 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2366 } elsif ( $conf->config('invoice_default_terms') ) {
2367 $msg .= ' - '. $conf->config('invoice_default_terms');
2374 my @display = scalar(@_)
2376 : qw( _items_previous _items_pkg );
2377 #: qw( _items_pkg );
2378 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2380 foreach my $display ( @display ) {
2381 push @b, $self->$display(@_);
2386 sub _items_previous {
2388 my $cust_main = $self->cust_main;
2389 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2391 foreach ( @pr_cust_bill ) {
2393 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2394 ' ('. time2str('%x',$_->_date). ')',
2395 #'pkgpart' => 'N/A',
2397 'amount' => sprintf("%.2f", $_->owed),
2403 # 'description' => 'Previous Balance',
2404 # #'pkgpart' => 'N/A',
2405 # 'pkgnum' => 'N/A',
2406 # 'amount' => sprintf("%10.2f", $pr_total ),
2407 # 'ext_description' => [ map {
2408 # "Invoice ". $_->invnum.
2409 # " (". time2str("%x",$_->_date). ") ".
2410 # sprintf("%10.2f", $_->owed)
2411 # } @pr_cust_bill ],
2418 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2419 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2424 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2425 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2428 sub _items_cust_bill_pkg {
2430 my $cust_bill_pkg = shift;
2433 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2435 my $desc = $cust_bill_pkg->desc;
2437 if ( $cust_bill_pkg->pkgnum > 0 ) {
2439 if ( $cust_bill_pkg->setup != 0 ) {
2440 my $description = $desc;
2441 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2442 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2443 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2445 description => $description,
2446 #pkgpart => $part_pkg->pkgpart,
2447 pkgnum => $cust_bill_pkg->pkgnum,
2448 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2449 ext_description => \@d,
2453 if ( $cust_bill_pkg->recur != 0 ) {
2455 description => "$desc (" .
2456 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2457 time2str('%x', $cust_bill_pkg->edate). ')',
2458 #pkgpart => $part_pkg->pkgpart,
2459 pkgnum => $cust_bill_pkg->pkgnum,
2460 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2462 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2463 $cust_bill_pkg->sdate),
2464 $cust_bill_pkg->details,
2469 } else { #pkgnum tax or one-shot line item (??)
2471 if ( $cust_bill_pkg->setup != 0 ) {
2473 'description' => $desc,
2474 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2477 if ( $cust_bill_pkg->recur != 0 ) {
2479 'description' => "$desc (".
2480 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2481 time2str("%x", $cust_bill_pkg->edate). ')',
2482 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2494 sub _items_credits {
2499 foreach ( $self->cust_credited ) {
2501 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2503 my $reason = $_->cust_credit->reason;
2504 #my $reason = substr($_->cust_credit->reason,0,32);
2505 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2506 $reason = " ($reason) " if $reason;
2508 #'description' => 'Credit ref\#'. $_->crednum.
2509 # " (". time2str("%x",$_->cust_credit->_date) .")".
2511 'description' => 'Credit applied '.
2512 time2str("%x",$_->cust_credit->_date). $reason,
2513 'amount' => sprintf("%.2f",$_->amount),
2516 #foreach ( @cr_cust_credit ) {
2518 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2519 # $money_char. sprintf("%10.2f",$_->credited)
2527 sub _items_payments {
2531 #get & print payments
2532 foreach ( $self->cust_bill_pay ) {
2534 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2537 'description' => "Payment received ".
2538 time2str("%x",$_->cust_pay->_date ),
2539 'amount' => sprintf("%.2f", $_->amount )
2558 sub process_reprint {
2559 process_re_X('print', @_);
2566 sub process_reemail {
2567 process_re_X('email', @_);
2575 process_re_X('fax', @_);
2578 use Storable qw(thaw);
2582 my( $method, $job ) = ( shift, shift );
2583 warn "process_re_X $method for job $job\n" if $DEBUG;
2585 my $param = thaw(decode_base64(shift));
2586 warn Dumper($param) if $DEBUG;
2597 my($method, $job, %param ) = @_;
2598 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2600 warn "re_X $method for job $job with param:\n".
2601 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
2604 #some false laziness w/search/cust_bill.html
2606 my $orderby = 'ORDER BY cust_bill._date';
2610 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2611 push @where, "cust_bill._date >= $1";
2613 if ( $param{'end'} =~ /^(\d+)$/ ) {
2614 push @where, "cust_bill._date < $1";
2616 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2617 push @where, "cust_main.agentnum = $1";
2621 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2622 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2623 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2624 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2626 push @where, "0 != $owed"
2629 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2632 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2634 my $addl_from = 'left join cust_main using ( custnum )';
2636 if ( $param{'newest_percust'} ) {
2637 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2638 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2639 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2642 my @cust_bill = qsearch( 'cust_bill',
2644 "$distinct cust_bill.*",
2650 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2651 foreach my $cust_bill ( @cust_bill ) {
2652 $cust_bill->$method();
2654 if ( $job ) { #progressbar foo
2656 if ( time - $min_sec > $last ) {
2657 my $error = $job->update_statustext(
2658 int( 100 * $num / scalar(@cust_bill) )
2660 die $error if $error;
2675 print_text formatting (and some logic :/) is in source, but needs to be
2676 slurped in from a file. Also number of lines ($=).
2680 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2681 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base