4 use vars qw( @ISA $DEBUG $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 );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
27 use FS::cust_bill_pay;
28 use FS::part_bill_event;
30 @ISA = qw( FS::cust_main_Mixin FS::Record );
34 #ask FS::UID to run this stuff for us later
35 FS::UID->install_callback( sub {
37 $money_char = $conf->config('money_char') || '$';
42 FS::cust_bill - Object methods for cust_bill records
48 $record = new FS::cust_bill \%hash;
49 $record = new FS::cust_bill { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
59 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
61 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
63 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
65 @cust_pay_objects = $cust_bill->cust_pay;
67 $tax_amount = $record->tax;
69 @lines = $cust_bill->print_text;
70 @lines = $cust_bill->print_text $time;
74 An FS::cust_bill object represents an invoice; a declaration that a customer
75 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
76 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
77 following fields are currently supported:
81 =item invnum - primary key (assigned automatically for new invoices)
83 =item custnum - customer (see L<FS::cust_main>)
85 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
86 L<Time::Local> and L<Date::Parse> for conversion functions.
88 =item charged - amount of this invoice
90 =item printed - deprecated
92 =item closed - books closed flag, empty or `Y'
102 Creates a new invoice. To add the invoice to the database, see L<"insert">.
103 Invoices are normally created by calling the bill method of a customer object
104 (see L<FS::cust_main>).
108 sub table { 'cust_bill'; }
110 sub cust_linked { $_[0]->cust_main_custnum; }
111 sub cust_unlinked_msg {
113 "WARNING: can't find cust_main.custnum ". $self->custnum.
114 ' (cust_bill.invnum '. $self->invnum. ')';
119 Adds this invoice to the database ("Posts" the invoice). If there is an error,
120 returns the error, otherwise returns false.
124 Currently unimplemented. I don't remove invoices because there would then be
125 no record you ever posted this invoice (which is bad, no?)
131 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
132 $self->SUPER::delete(@_);
135 =item replace OLD_RECORD
137 Replaces the OLD_RECORD with this one in the database. If there is an error,
138 returns the error, otherwise returns false.
140 Only printed may be changed. printed is normally updated by calling the
141 collect method of a customer object (see L<FS::cust_main>).
146 my( $new, $old ) = ( shift, shift );
147 return "Can't change custnum!" unless $old->custnum == $new->custnum;
148 #return "Can't change _date!" unless $old->_date eq $new->_date;
149 return "Can't change _date!" unless $old->_date == $new->_date;
150 return "Can't change charged!" unless $old->charged == $new->charged;
152 $new->SUPER::replace($old);
157 Checks all fields to make sure this is a valid invoice. If there is an error,
158 returns the error, otherwise returns false. Called by the insert and replace
167 $self->ut_numbern('invnum')
168 || $self->ut_number('custnum')
169 || $self->ut_numbern('_date')
170 || $self->ut_money('charged')
171 || $self->ut_numbern('printed')
172 || $self->ut_enum('closed', [ '', 'Y' ])
174 return $error if $error;
176 return "Unknown customer"
177 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
179 $self->_date(time) unless $self->_date;
181 $self->printed(0) if $self->printed eq '';
188 Returns a list consisting of the total previous balance for this customer,
189 followed by the previous outstanding invoices (as FS::cust_bill objects also).
196 my @cust_bill = sort { $a->_date <=> $b->_date }
197 grep { $_->owed != 0 && $_->_date < $self->_date }
198 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
200 foreach ( @cust_bill ) { $total += $_->owed; }
206 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
212 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
215 =item cust_bill_event
217 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
222 sub cust_bill_event {
224 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
230 Returns the customer (see L<FS::cust_main>) for this invoice.
236 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
241 Depreciated. See the cust_credited method.
243 #Returns a list consisting of the total previous credited (see
244 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
245 #outstanding credits (FS::cust_credit objects).
251 croak "FS::cust_bill->cust_credit depreciated; see ".
252 "FS::cust_bill->cust_credit_bill";
255 #my @cust_credit = sort { $a->_date <=> $b->_date }
256 # grep { $_->credited != 0 && $_->_date < $self->_date }
257 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
259 #foreach (@cust_credit) { $total += $_->credited; }
260 #$total, @cust_credit;
265 Depreciated. See the cust_bill_pay method.
267 #Returns all payments (see L<FS::cust_pay>) for this invoice.
273 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
275 #sort { $a->_date <=> $b->_date }
276 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
282 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
288 sort { $a->_date <=> $b->_date }
289 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
294 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
300 sort { $a->_date <=> $b->_date }
301 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
307 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
314 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
316 foreach (@taxlines) { $total += $_->setup; }
322 Returns the amount owed (still outstanding) on this invoice, which is charged
323 minus all payment applications (see L<FS::cust_bill_pay>) and credit
324 applications (see L<FS::cust_credit_bill>).
330 my $balance = $self->charged;
331 $balance -= $_->amount foreach ( $self->cust_bill_pay );
332 $balance -= $_->amount foreach ( $self->cust_credited );
333 $balance = sprintf( "%.2f", $balance);
334 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
339 =item generate_email PARAMHASH
341 PARAMHASH can contain the following:
345 =item from => sender address, required
347 =item tempate => alternate template name, optional
349 =item print_text => text attachment arrayref, optional
351 =item subject => email subject, optional
355 Returns an argument list to be passed to L<FS::Misc::send_email>.
366 my $me = '[FS::cust_bill::generate_email]';
369 'from' => $args{'from'},
370 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
373 if (ref($args{'to'} eq 'ARRAY')) {
374 $return{'to'} = $args{'to'};
376 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
377 $self->cust_main->invoicing_list
381 if ( $conf->exists('invoice_html') ) {
383 warn "$me creating HTML/text multipart message"
386 $return{'nobody'} = 1;
388 my $alternative = build MIME::Entity
389 'Type' => 'multipart/alternative',
390 'Encoding' => '7bit',
391 'Disposition' => 'inline'
395 if ( $conf->exists('invoice_email_pdf')
396 and scalar($conf->config('invoice_email_pdf_note')) ) {
398 warn "$me using 'invoice_email_pdf_note' in multipart message"
400 $data = [ map { $_ . "\n" }
401 $conf->config('invoice_email_pdf_note')
406 warn "$me not using 'invoice_email_pdf_note' in multipart message"
408 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
409 $data = $args{'print_text'};
411 $data = [ $self->print_text('', $args{'template'}) ];
416 $alternative->attach(
417 'Type' => 'text/plain',
418 #'Encoding' => 'quoted-printable',
419 'Encoding' => '7bit',
421 'Disposition' => 'inline',
424 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
425 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
427 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
429 if ( defined($args{'_template'}) && length($args{'_template'})
430 && -e "$path/logo_". $args{'_template'}. ".png"
433 $file = "$path/logo_". $args{'_template'}. ".png";
435 $file = "$path/logo.png";
438 my $image = build MIME::Entity
439 'Type' => 'image/png',
440 'Encoding' => 'base64',
442 'Filename' => 'logo.png',
443 'Content-ID' => "<$content_id>",
446 $alternative->attach(
447 'Type' => 'text/html',
448 'Encoding' => 'quoted-printable',
449 'Data' => [ '<html>',
452 ' '. encode_entities($return{'subject'}),
455 ' <body bgcolor="#e8e8e8">',
456 $self->print_html('', $args{'template'}, $content_id),
460 'Disposition' => 'inline',
461 #'Filename' => 'invoice.pdf',
464 if ( $conf->exists('invoice_email_pdf') ) {
469 # multipart/alternative
475 my $related = build MIME::Entity 'Type' => 'multipart/related',
476 'Encoding' => '7bit';
478 #false laziness w/Misc::send_email
479 $related->head->replace('Content-type',
481 '; boundary="'. $related->head->multipart_boundary. '"'.
482 '; type=multipart/alternative'
485 $related->add_part($alternative);
487 $related->add_part($image);
489 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
491 $return{'mimeparts'} = [ $related, $pdf ];
495 #no other attachment:
497 # multipart/alternative
502 $return{'content-type'} = 'multipart/related';
503 $return{'mimeparts'} = [ $alternative, $image ];
504 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
505 #$return{'disposition'} = 'inline';
511 if ( $conf->exists('invoice_email_pdf') ) {
512 warn "$me creating PDF attachment"
515 #mime parts arguments a la MIME::Entity->build().
516 $return{'mimeparts'} = [
517 { $self->mimebuild_pdf('', $args{'template'}) }
521 if ( $conf->exists('invoice_email_pdf')
522 and scalar($conf->config('invoice_email_pdf_note')) ) {
524 warn "$me using 'invoice_email_pdf_note'"
526 $return{'body'} = [ map { $_ . "\n" }
527 $conf->config('invoice_email_pdf_note')
532 warn "$me not using 'invoice_email_pdf_note'"
534 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
535 $return{'body'} = $args{'print_text'};
537 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
550 Returns a list suitable for passing to MIME::Entity->build(), representing
551 this invoice as PDF attachment.
558 'Type' => 'application/pdf',
559 'Encoding' => 'base64',
560 'Data' => [ $self->print_pdf(@_) ],
561 'Disposition' => 'attachment',
562 'Filename' => 'invoice.pdf',
566 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
568 Sends this invoice to the destinations configured for this customer: sends
569 email, prints and/or faxes. See L<FS::cust_main_invoice>.
571 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
573 AGENTNUM, if specified, means that this invoice will only be sent for customers
574 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
575 single agent) or an arrayref of agentnums.
577 INVOICE_FROM, if specified, overrides the default email invoice From: address.
583 my $template = scalar(@_) ? shift : '';
584 if ( scalar(@_) && $_[0] ) {
585 my $agentnums = ref($_[0]) ? shift : [ shift ];
586 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
592 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
594 my @invoicing_list = $self->cust_main->invoicing_list;
596 $self->email($template, $invoice_from)
597 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
599 $self->print($template)
600 if grep { $_ eq 'POST' } @invoicing_list; #postal
602 $self->fax($template)
603 if grep { $_ eq 'FAX' } @invoicing_list; #fax
609 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
613 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
615 INVOICE_FROM, if specified, overrides the default email invoice From: address.
621 my $template = scalar(@_) ? shift : '';
625 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
627 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
628 $self->cust_main->invoicing_list;
630 #better to notify this person than silence
631 @invoicing_list = ($invoice_from) unless @invoicing_list;
633 my $error = send_email(
634 $self->generate_email(
635 'from' => $invoice_from,
636 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
637 'template' => $template,
640 die "can't email invoice: $error\n" if $error;
641 #die "$error\n" if $error;
645 =item lpr_data [ TEMPLATENAME ]
647 Returns the postscript or plaintext for this invoice as an arrayref.
649 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
654 my( $self, $template) = @_;
655 $conf->exists('invoice_latex')
656 ? [ $self->print_ps('', $template) ]
657 : [ $self->print_text('', $template) ];
660 =item print [ TEMPLATENAME ]
664 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
670 my $template = scalar(@_) ? shift : '';
672 my $lpr = $conf->config('lpr');
675 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
677 $outerr = ": $outerr" if length($outerr);
678 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
683 =item fax [ TEMPLATENAME ]
687 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
693 my $template = scalar(@_) ? shift : '';
695 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
696 unless $conf->exists('invoice_latex');
698 my $dialstring = $self->cust_main->getfield('fax');
701 my $error = send_fax( 'docdata' => $self->lpr_data($template),
702 'dialstring' => $dialstring,
704 die $error if $error;
708 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
710 Like B<send>, but only sends the invoice if it is the newest open invoice for
720 grep { $_->owed > 0 }
721 qsearch('cust_bill', {
722 'custnum' => $self->custnum,
723 #'_date' => { op=>'>', value=>$self->_date },
724 'invnum' => { op=>'>', value=>$self->invnum },
731 =item send_csv OPTION => VALUE, ...
733 Sends invoice as a CSV data-file to a remote host with the specified protocol.
737 protocol - currently only "ftp"
742 format - 'default' or 'billco'
745 If I<format> is not specified or "default", the file will be named
746 "N-YYYYMMDDHHMMSS.csv" where N is the invoice number and YYMMDDHHMMSS is a
750 If I<format> is "billco", two files will be created and uploaded. They will be named "N-YYYYMMDDHHMMSS-header.csv" and "N-YYYYMMDDHHMMSS-detail.csv" where N
751 is the invoice number and YYMMDDHHMMSS is a timestamp(???).
753 See L</print_csv> for a description of the output format.
758 my($self, %opt) = @_;
762 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
763 mkdir $spooldir, 0700 unless -d $spooldir;
765 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
766 my $file = "$spooldir/$tracctnum";
767 if ( lc($opt{'format'}) eq 'billco' ) {
768 $file .= '-header.csv';
770 #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
774 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
776 open(CSV, ">$file") or die "can't open $file: $!";
780 if ( lc($opt{'format'}) eq 'billco' ) {
783 $file = "$spooldir/$tracctnum-detail.csv";
784 open(CSV,">$file") or die "can't open $file: $!";
792 if ( $opt{protocol} eq 'ftp' ) {
793 eval "use Net::FTP;";
795 $net = Net::FTP->new($opt{server}) or die @$;
797 die "unknown protocol: $opt{protocol}";
800 $net->login( $opt{username}, $opt{password} )
801 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
803 $net->binary or die "can't set binary mode";
805 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
808 $net->put($oldfile) or die "can't put $oldfile: $!";
810 $net->put($file) or die "can't put $file: $!";
814 unlink $oldfile if $oldfile;
821 Spools CSV invoice data.
827 =item format - 'default' or 'billco'
829 =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>).
836 my($self, %opt) = @_;
838 if ( $opt{'dest'} ) {
839 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
840 $self->cust_main->invoicing_list;
841 return unless $invoicing_list{$opt{'dest'}};
846 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
847 mkdir $spooldir, 0700 unless -d $spooldir;
849 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
850 my $file = "$spooldir/spool";
851 if ( lc($opt{'format'}) eq 'billco' ) {
852 $file .= '-header.csv';
854 #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
858 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
860 open(CSV, ">>$file") or die "can't open $file: $!";
867 if ( lc($opt{'format'}) eq 'billco' ) {
873 $file = "$spooldir/spool-detail.csv";
875 open(CSV,">>$file") or die "can't open $file: $!";
889 =item print_csv OPTION => VALUE, ...
891 Returns CSV data for this invoice.
895 format - 'default' or 'billco'
897 Returns a list consisting of two scalars. The first is a single line of CSV
898 header information for this invoice. The second is one or more lines of CSV
899 detail information for this invoice.
901 If I<format> is not specified or "default", the fields of the CSV file are as
904 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
908 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
910 B<record_type> is C<cust_bill> for the initial header line only. The
911 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
912 fields are filled in.
914 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
915 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
918 =item invnum - invoice number
920 =item custnum - customer number
922 =item _date - invoice date
924 =item charged - total invoice amount
926 =item first - customer first name
928 =item last - customer first name
930 =item company - company name
932 =item address1 - address line 1
934 =item address2 - address line 1
944 =item pkg - line item description
946 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
948 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
950 =item sdate - start date for recurring fee
952 =item edate - end date for recurring fee
956 If I<format> is "billco", the fields of the header CSV file are as follows:
958 +-------------------------------------------------------------------+
959 | FORMAT HEADER FILE |
960 |-------------------------------------------------------------------|
961 | Field | Description | Name | Type | Width |
962 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
963 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
964 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
965 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
966 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
967 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
968 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
969 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
970 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
971 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
972 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
973 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
974 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
975 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
976 | 15 | Previous Balance | BALFWD | NUM* | 9 |
977 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
978 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
979 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
980 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
981 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
982 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
983 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
984 | 23 | Y/N | AGESWITCH | CHAR | 1 |
985 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
986 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
987 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
988 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
989 | 28 | State Tax*** | STATETAX | NUM* | 9 |
990 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
991 +-------+-------------------------------+------------+------+-------+
993 If I<format> is "billco", the fields of the detail CSV file are as follows:
995 FORMAT FOR DETAIL FILE
997 Field | Description | Name | Type | Width
998 1 | N/A-Leave Empty | RC | CHAR | 2
999 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1000 3 | Account Number | TRACCTNUM | CHAR | 15
1001 4 | Invoice Number | TRINVOICE | CHAR | 15
1002 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1003 6 | Transaction Detail | DETAILS | CHAR | 100
1004 7 | Amount | AMT | NUM* | 9
1005 8 | Line Format Control** | LNCTRL | CHAR | 2
1006 9 | Grouping Code | GROUP | CHAR | 2
1007 10 | User Defined | ACCT CODE | CHAR | 15
1012 my($self, %opt) = @_;
1014 eval "use Text::CSV_XS";
1017 my $cust_main = $self->cust_main;
1019 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1021 if ( lc($opt{'format'}) eq 'billco' ) {
1024 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1027 if ( $conf->exists('invoice_default_terms')
1028 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1029 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1032 my( $previous_balance, @unused ) = $self->previous; #previous balance
1034 my $pmt_cr_applied = 0;
1035 $pmt_cr_applied += $_->{'amount'}
1036 foreach ( $self->_items_payments, $self->_items_credits ) ;
1038 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1041 '', # 1 | N/A-Leave Empty CHAR 2
1042 '', # 2 | N/A-Leave Empty CHAR 15
1043 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1044 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1045 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1046 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1047 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1048 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1049 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1050 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1051 '', # 10 | Ancillary Billing Information CHAR 30
1052 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1053 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1056 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1059 $duedate, # 14 | Bill Due Date CHAR 10
1061 $previous_balance, # 15 | Previous Balance NUM* 9
1062 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1063 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1064 $totaldue, # 18 | Total Amt Due NUM* 9
1065 $totaldue, # 19 | Total Amt Due NUM* 9
1066 '', # 20 | 30 Day Aging NUM* 9
1067 '', # 21 | 60 Day Aging NUM* 9
1068 '', # 22 | 90 Day Aging NUM* 9
1069 'N', # 23 | Y/N CHAR 1
1070 '', # 24 | Remittance automation CHAR 100
1071 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1072 $self->custnum, # 26 | Customer Reference Number CHAR 15
1073 '0', # 27 | Federal Tax*** NUM* 9
1074 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1075 '0', # 29 | Other Taxes & Fees*** NUM* 9
1084 time2str("%x", $self->_date),
1085 sprintf("%.2f", $self->charged),
1086 ( map { $cust_main->getfield($_) }
1087 qw( first last company address1 address2 city state zip country ) ),
1089 ) or die "can't create csv";
1092 my $header = $csv->string. "\n";
1095 if ( lc($opt{'format'}) eq 'billco' ) {
1098 foreach my $item ( $self->_items_pkg ) {
1101 '', # 1 | N/A-Leave Empty CHAR 2
1102 '', # 2 | N/A-Leave Empty CHAR 15
1103 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1104 $self->invnum, # 4 | Invoice Number CHAR 15
1105 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1106 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1107 $item->{'amount'}, # 7 | Amount NUM* 9
1108 '', # 8 | Line Format Control** CHAR 2
1109 '', # 9 | Grouping Code CHAR 2
1110 '', # 10 | User Defined CHAR 15
1113 $detail .= $csv->string. "\n";
1119 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1121 my($pkg, $setup, $recur, $sdate, $edate);
1122 if ( $cust_bill_pkg->pkgnum ) {
1124 ($pkg, $setup, $recur, $sdate, $edate) = (
1125 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1126 ( $cust_bill_pkg->setup != 0
1127 ? sprintf("%.2f", $cust_bill_pkg->setup )
1129 ( $cust_bill_pkg->recur != 0
1130 ? sprintf("%.2f", $cust_bill_pkg->recur )
1132 ( $cust_bill_pkg->sdate
1133 ? time2str("%x", $cust_bill_pkg->sdate)
1135 ($cust_bill_pkg->edate
1136 ?time2str("%x", $cust_bill_pkg->edate)
1140 } else { #pkgnum tax
1141 next unless $cust_bill_pkg->setup != 0;
1142 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1143 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1145 ($pkg, $setup, $recur, $sdate, $edate) =
1146 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1152 ( map { '' } (1..11) ),
1153 ($pkg, $setup, $recur, $sdate, $edate)
1154 ) or die "can't create csv";
1156 $detail .= $csv->string. "\n";
1162 ( $header, $detail );
1168 Pays this invoice with a compliemntary payment. If there is an error,
1169 returns the error, otherwise returns false.
1175 my $cust_pay = new FS::cust_pay ( {
1176 'invnum' => $self->invnum,
1177 'paid' => $self->owed,
1180 'payinfo' => $self->cust_main->payinfo,
1188 Attempts to pay this invoice with a credit card payment via a
1189 Business::OnlinePayment realtime gateway. See
1190 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1191 for supported processors.
1197 $self->realtime_bop( 'CC', @_ );
1202 Attempts to pay this invoice with an electronic check (ACH) payment via a
1203 Business::OnlinePayment realtime gateway. See
1204 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1205 for supported processors.
1211 $self->realtime_bop( 'ECHECK', @_ );
1216 Attempts to pay this invoice with phone bill (LEC) payment via a
1217 Business::OnlinePayment realtime gateway. See
1218 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1219 for supported processors.
1225 $self->realtime_bop( 'LEC', @_ );
1229 my( $self, $method ) = @_;
1231 my $cust_main = $self->cust_main;
1232 my $balance = $cust_main->balance;
1233 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1234 $amount = sprintf("%.2f", $amount);
1235 return "not run (balance $balance)" unless $amount > 0;
1237 my $description = 'Internet Services';
1238 if ( $conf->exists('business-onlinepayment-description') ) {
1239 my $dtempl = $conf->config('business-onlinepayment-description');
1241 my $agent_obj = $cust_main->agent
1242 or die "can't retreive agent for $cust_main (agentnum ".
1243 $cust_main->agentnum. ")";
1244 my $agent = $agent_obj->agent;
1245 my $pkgs = join(', ',
1246 map { $_->cust_pkg->part_pkg->pkg }
1247 grep { $_->pkgnum } $self->cust_bill_pkg
1249 $description = eval qq("$dtempl");
1252 $cust_main->realtime_bop($method, $amount,
1253 'description' => $description,
1254 'invnum' => $self->invnum,
1261 Adds a payment for this invoice to the pending credit card batch (see
1262 L<FS::cust_pay_batch>).
1268 my $cust_main = $self->cust_main;
1270 my $cust_pay_batch = new FS::cust_pay_batch ( {
1271 'invnum' => $self->getfield('invnum'),
1272 'custnum' => $cust_main->getfield('custnum'),
1273 'last' => $cust_main->getfield('last'),
1274 'first' => $cust_main->getfield('first'),
1275 'address1' => $cust_main->getfield('address1'),
1276 'address2' => $cust_main->getfield('address2'),
1277 'city' => $cust_main->getfield('city'),
1278 'state' => $cust_main->getfield('state'),
1279 'zip' => $cust_main->getfield('zip'),
1280 'country' => $cust_main->getfield('country'),
1281 'cardnum' => $cust_main->payinfo,
1282 'exp' => $cust_main->getfield('paydate'),
1283 'payname' => $cust_main->getfield('payname'),
1284 'amount' => $self->owed,
1286 my $error = $cust_pay_batch->insert;
1287 die $error if $error;
1292 sub _agent_template {
1294 $self->_agent_plandata('agent_templatename');
1297 sub _agent_invoice_from {
1299 $self->_agent_plandata('agent_invoice_from');
1302 sub _agent_plandata {
1303 my( $self, $option ) = @_;
1305 my $part_bill_event = qsearchs( 'part_bill_event',
1307 'payby' => $self->cust_main->payby,
1308 'plan' => 'send_agent',
1309 'plandata' => { 'op' => '~',
1310 'value' => "(^|\n)agentnum ".
1312 $self->cust_main->agentnum.
1318 'ORDER BY seconds LIMIT 1'
1321 return '' unless $part_bill_event;
1323 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1326 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1327 " plandata for $option";
1333 =item print_text [ TIME [ , TEMPLATE ] ]
1335 Returns an text invoice, as a list of lines.
1337 TIME an optional value used to control the printing of overdue messages. The
1338 default is now. It isn't the date of the invoice; that's the `_date' field.
1339 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1340 L<Time::Local> and L<Date::Parse> for conversion functions.
1344 #still some false laziness w/_items stuff (and send_csv)
1347 my( $self, $today, $template ) = @_;
1350 # my $invnum = $self->invnum;
1351 my $cust_main = $self->cust_main;
1352 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1353 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1355 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1356 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1357 #my $balance_due = $self->owed + $pr_total - $cr_total;
1358 my $balance_due = $self->owed + $pr_total;
1361 #my($description,$amount);
1365 foreach ( @pr_cust_bill ) {
1367 "Previous Balance, Invoice #". $_->invnum.
1368 " (". time2str("%x",$_->_date). ")",
1369 $money_char. sprintf("%10.2f",$_->owed)
1372 if (@pr_cust_bill) {
1373 push @buf,['','-----------'];
1374 push @buf,[ 'Total Previous Balance',
1375 $money_char. sprintf("%10.2f",$pr_total ) ];
1380 foreach my $cust_bill_pkg (
1381 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1382 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1385 my $desc = $cust_bill_pkg->desc;
1387 if ( $cust_bill_pkg->pkgnum > 0 ) {
1389 if ( $cust_bill_pkg->setup != 0 ) {
1390 my $description = $desc;
1391 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1392 push @buf, [ $description,
1393 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1395 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1396 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1399 if ( $cust_bill_pkg->recur != 0 ) {
1401 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1402 time2str("%x", $cust_bill_pkg->edate) . ")",
1403 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1406 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1407 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1408 $cust_bill_pkg->sdate );
1411 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1413 } else { #pkgnum tax or one-shot line item
1415 if ( $cust_bill_pkg->setup != 0 ) {
1417 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1419 if ( $cust_bill_pkg->recur != 0 ) {
1420 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1421 . time2str("%x", $cust_bill_pkg->edate). ")",
1422 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1430 push @buf,['','-----------'];
1431 push @buf,['Total New Charges',
1432 $money_char. sprintf("%10.2f",$self->charged) ];
1435 push @buf,['','-----------'];
1436 push @buf,['Total Charges',
1437 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1441 foreach ( $self->cust_credited ) {
1443 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1445 my $reason = substr($_->cust_credit->reason,0,32);
1446 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1447 $reason = " ($reason) " if $reason;
1449 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1451 $money_char. sprintf("%10.2f",$_->amount)
1454 #foreach ( @cr_cust_credit ) {
1456 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1457 # $money_char. sprintf("%10.2f",$_->credited)
1461 #get & print payments
1462 foreach ( $self->cust_bill_pay ) {
1464 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1467 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1468 $money_char. sprintf("%10.2f",$_->amount )
1473 my $balance_due_msg = $self->balance_due_msg;
1475 push @buf,['','-----------'];
1476 push @buf,[$balance_due_msg, $money_char.
1477 sprintf("%10.2f", $balance_due ) ];
1479 #create the template
1480 $template ||= $self->_agent_template;
1481 my $templatefile = 'invoice_template';
1482 $templatefile .= "_$template" if length($template);
1483 my @invoice_template = $conf->config($templatefile)
1484 or die "cannot load config file $templatefile";
1487 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1488 /invoice_lines\((\d*)\)/;
1489 $invoice_lines += $1 || scalar(@buf);
1492 die "no invoice_lines() functions in template?" unless $wasfunc;
1493 my $invoice_template = new Text::Template (
1495 SOURCE => [ map "$_\n", @invoice_template ],
1496 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1497 $invoice_template->compile()
1498 or die "can't compile template: $Text::Template::ERROR";
1500 #setup template variables
1501 package FS::cust_bill::_template; #!
1502 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1504 $invnum = $self->invnum;
1505 $date = $self->_date;
1507 $agent = $self->cust_main->agent->agent;
1509 if ( $FS::cust_bill::invoice_lines ) {
1511 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1513 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1518 #format address (variable for the template)
1520 @address = ( '', '', '', '', '', '' );
1521 package FS::cust_bill; #!
1522 $FS::cust_bill::_template::address[$l++] =
1523 $cust_main->payname.
1524 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1525 ? " (P.O. #". $cust_main->payinfo. ")"
1529 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1530 if $cust_main->company;
1531 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1532 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1533 if $cust_main->address2;
1534 $FS::cust_bill::_template::address[$l++] =
1535 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1537 my $countrydefault = $conf->config('countrydefault') || 'US';
1538 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1539 unless $cust_main->country eq $countrydefault;
1541 # #overdue? (variable for the template)
1542 # $FS::cust_bill::_template::overdue = (
1544 # && $today > $self->_date
1545 ## && $self->printed > 1
1546 # && $self->printed > 0
1549 #and subroutine for the template
1550 sub FS::cust_bill::_template::invoice_lines {
1551 my $lines = shift || scalar(@buf);
1553 scalar(@buf) ? shift @buf : [ '', '' ];
1559 $FS::cust_bill::_template::page = 1;
1563 push @collect, split("\n",
1564 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1566 $FS::cust_bill::_template::page++;
1569 map "$_\n", @collect;
1573 =item print_latex [ TIME [ , TEMPLATE ] ]
1575 Internal method - returns a filename of a filled-in LaTeX template for this
1576 invoice (Note: add ".tex" to get the actual filename).
1578 See print_ps and print_pdf for methods that return PostScript and PDF output.
1580 TIME an optional value used to control the printing of overdue messages. The
1581 default is now. It isn't the date of the invoice; that's the `_date' field.
1582 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1583 L<Time::Local> and L<Date::Parse> for conversion functions.
1587 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1590 my( $self, $today, $template ) = @_;
1592 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1595 my $cust_main = $self->cust_main;
1596 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1597 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1599 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1600 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1601 #my $balance_due = $self->owed + $pr_total - $cr_total;
1602 my $balance_due = $self->owed + $pr_total;
1604 #create the template
1605 $template ||= $self->_agent_template;
1606 my $templatefile = 'invoice_latex';
1607 my $suffix = length($template) ? "_$template" : '';
1608 $templatefile .= $suffix;
1609 my @invoice_template = map "$_\n", $conf->config($templatefile)
1610 or die "cannot load config file $templatefile";
1612 my($format, $text_template);
1613 if ( grep { /^%%Detail/ } @invoice_template ) {
1614 #change this to a die when the old code is removed
1615 warn "old-style invoice template $templatefile; ".
1616 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1619 $format = 'Text::Template';
1620 $text_template = new Text::Template(
1622 SOURCE => \@invoice_template,
1623 DELIMITERS => [ '[@--', '--@]' ],
1626 $text_template->compile()
1627 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1631 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1632 $returnaddress = join("\n",
1633 $conf->config_orbase('invoice_latexreturnaddress', $template)
1636 $returnaddress = '~';
1639 my %invoice_data = (
1640 'invnum' => $self->invnum,
1641 'date' => time2str('%b %o, %Y', $self->_date),
1642 'today' => time2str('%b %o, %Y', $today),
1643 'agent' => _latex_escape($cust_main->agent->agent),
1644 'payname' => _latex_escape($cust_main->payname),
1645 'company' => _latex_escape($cust_main->company),
1646 'address1' => _latex_escape($cust_main->address1),
1647 'address2' => _latex_escape($cust_main->address2),
1648 'city' => _latex_escape($cust_main->city),
1649 'state' => _latex_escape($cust_main->state),
1650 'zip' => _latex_escape($cust_main->zip),
1651 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1652 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1653 'returnaddress' => $returnaddress,
1655 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1656 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1657 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1660 my $countrydefault = $conf->config('countrydefault') || 'US';
1661 if ( $cust_main->country eq $countrydefault ) {
1662 $invoice_data{'country'} = '';
1664 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1667 $invoice_data{'notes'} =
1669 # #do variable substitutions in notes
1670 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1671 $conf->config_orbase('invoice_latexnotes', $template)
1673 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1676 $invoice_data{'footer'} =~ s/\n+$//;
1677 $invoice_data{'smallfooter'} =~ s/\n+$//;
1678 $invoice_data{'notes'} =~ s/\n+$//;
1680 $invoice_data{'po_line'} =
1681 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1682 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1686 if ( $format eq 'old' ) {
1689 my @total_item = ();
1690 while ( @invoice_template ) {
1691 my $line = shift @invoice_template;
1693 if ( $line =~ /^%%Detail\s*$/ ) {
1695 while ( ( my $line_item_line = shift @invoice_template )
1696 !~ /^%%EndDetail\s*$/ ) {
1697 push @line_item, $line_item_line;
1699 foreach my $line_item ( $self->_items ) {
1700 #foreach my $line_item ( $self->_items_pkg ) {
1701 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1702 $invoice_data{'description'} =
1703 _latex_escape($line_item->{'description'});
1704 if ( exists $line_item->{'ext_description'} ) {
1705 $invoice_data{'description'} .=
1706 "\\tabularnewline\n~~".
1707 join( "\\tabularnewline\n~~",
1708 map _latex_escape($_), @{$line_item->{'ext_description'}}
1711 $invoice_data{'amount'} = $line_item->{'amount'};
1712 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1714 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1717 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1719 while ( ( my $total_item_line = shift @invoice_template )
1720 !~ /^%%EndTotalDetails\s*$/ ) {
1721 push @total_item, $total_item_line;
1724 my @total_fill = ();
1727 foreach my $tax ( $self->_items_tax ) {
1728 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1729 $taxtotal += $tax->{'amount'};
1730 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1732 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1737 $invoice_data{'total_item'} = 'Sub-total';
1738 $invoice_data{'total_amount'} =
1739 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1740 unshift @total_fill,
1741 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1745 $invoice_data{'total_item'} = '\textbf{Total}';
1746 $invoice_data{'total_amount'} =
1747 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1749 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1752 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1755 foreach my $credit ( $self->_items_credits ) {
1756 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1758 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1760 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1765 foreach my $payment ( $self->_items_payments ) {
1766 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1768 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1770 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1774 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1775 $invoice_data{'total_amount'} =
1776 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1778 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1781 push @filled_in, @total_fill;
1784 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1785 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1786 push @filled_in, $line;
1797 } elsif ( $format eq 'Text::Template' ) {
1799 my @detail_items = ();
1800 my @total_items = ();
1802 $invoice_data{'detail_items'} = \@detail_items;
1803 $invoice_data{'total_items'} = \@total_items;
1805 foreach my $line_item ( $self->_items ) {
1807 ext_description => [],
1809 $detail->{'ref'} = $line_item->{'pkgnum'};
1810 $detail->{'quantity'} = 1;
1811 $detail->{'description'} = _latex_escape($line_item->{'description'});
1812 if ( exists $line_item->{'ext_description'} ) {
1813 @{$detail->{'ext_description'}} = map {
1815 } @{$line_item->{'ext_description'}};
1817 $detail->{'amount'} = $line_item->{'amount'};
1818 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1820 push @detail_items, $detail;
1825 foreach my $tax ( $self->_items_tax ) {
1827 $total->{'total_item'} = _latex_escape($tax->{'description'});
1828 $taxtotal += $tax->{'amount'};
1829 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1830 push @total_items, $total;
1835 $total->{'total_item'} = 'Sub-total';
1836 $total->{'total_amount'} =
1837 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1838 unshift @total_items, $total;
1843 $total->{'total_item'} = '\textbf{Total}';
1844 $total->{'total_amount'} =
1845 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1846 push @total_items, $total;
1849 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1852 foreach my $credit ( $self->_items_credits ) {
1854 $total->{'total_item'} = _latex_escape($credit->{'description'});
1856 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1857 push @total_items, $total;
1861 foreach my $payment ( $self->_items_payments ) {
1863 $total->{'total_item'} = _latex_escape($payment->{'description'});
1865 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1866 push @total_items, $total;
1871 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1872 $total->{'total_amount'} =
1873 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1874 push @total_items, $total;
1878 die "guru meditation #54";
1881 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1882 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1886 ) or die "can't open temp file: $!\n";
1887 if ( $format eq 'old' ) {
1888 print $fh join('', @filled_in );
1889 } elsif ( $format eq 'Text::Template' ) {
1890 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1892 die "guru meditation #32";
1896 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1901 =item print_ps [ TIME [ , TEMPLATE ] ]
1903 Returns an postscript invoice, as a scalar.
1905 TIME an optional value used to control the printing of overdue messages. The
1906 default is now. It isn't the date of the invoice; that's the `_date' field.
1907 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1908 L<Time::Local> and L<Date::Parse> for conversion functions.
1915 my $file = $self->print_latex(@_);
1917 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1920 my $sfile = shell_quote $file;
1922 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1923 or die "pslatex $file.tex failed; see $file.log for details?\n";
1924 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1925 or die "pslatex $file.tex failed; see $file.log for details?\n";
1927 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1928 or die "dvips failed";
1930 open(POSTSCRIPT, "<$file.ps")
1931 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1933 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1936 while (<POSTSCRIPT>) {
1946 =item print_pdf [ TIME [ , TEMPLATE ] ]
1948 Returns an PDF invoice, as a scalar.
1950 TIME an optional value used to control the printing of overdue messages. The
1951 default is now. It isn't the date of the invoice; that's the `_date' field.
1952 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1953 L<Time::Local> and L<Date::Parse> for conversion functions.
1960 my $file = $self->print_latex(@_);
1962 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1965 #system('pdflatex', "$file.tex");
1966 #system('pdflatex', "$file.tex");
1967 #! LaTeX Error: Unknown graphics extension: .eps.
1969 my $sfile = shell_quote $file;
1971 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1972 or die "pslatex $file.tex failed; see $file.log for details?\n";
1973 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1974 or die "pslatex $file.tex failed; see $file.log for details?\n";
1976 #system('dvipdf', "$file.dvi", "$file.pdf" );
1978 "dvips -q -t letter -f $sfile.dvi ".
1979 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1982 or die "dvips | gs failed: $!";
1984 open(PDF, "<$file.pdf")
1985 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1987 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2000 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2002 Returns an HTML invoice, as a scalar.
2004 TIME an optional value used to control the printing of overdue messages. The
2005 default is now. It isn't the date of the invoice; that's the `_date' field.
2006 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2007 L<Time::Local> and L<Date::Parse> for conversion functions.
2009 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2010 when emailing the invoice as part of a multipart/related MIME email.
2014 #some falze laziness w/print_text and print_latex (and send_csv)
2016 my( $self, $today, $template, $cid ) = @_;
2019 my $cust_main = $self->cust_main;
2020 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2021 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2023 $template ||= $self->_agent_template;
2024 my $templatefile = 'invoice_html';
2025 my $suffix = length($template) ? "_$template" : '';
2026 $templatefile .= $suffix;
2027 my @html_template = map "$_\n", $conf->config($templatefile)
2028 or die "cannot load config file $templatefile";
2030 my $html_template = new Text::Template(
2032 SOURCE => \@html_template,
2033 DELIMITERS => [ '<%=', '%>' ],
2036 $html_template->compile()
2037 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2039 my %invoice_data = (
2040 'invnum' => $self->invnum,
2041 'date' => time2str('%b %o, %Y', $self->_date),
2042 'today' => time2str('%b %o, %Y', $today),
2043 'agent' => encode_entities($cust_main->agent->agent),
2044 'payname' => encode_entities($cust_main->payname),
2045 'company' => encode_entities($cust_main->company),
2046 'address1' => encode_entities($cust_main->address1),
2047 'address2' => encode_entities($cust_main->address2),
2048 'city' => encode_entities($cust_main->city),
2049 'state' => encode_entities($cust_main->state),
2050 'zip' => encode_entities($cust_main->zip),
2051 'terms' => $conf->config('invoice_default_terms')
2052 || 'Payable upon receipt',
2054 'template' => $template,
2055 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2059 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2060 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2062 $invoice_data{'returnaddress'} =
2063 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2065 $invoice_data{'returnaddress'} =
2068 s/\\\\\*?\s*$/<BR>/;
2069 s/\\hyphenation\{[\w\s\-]+\}//;
2072 $conf->config_orbase( 'invoice_latexreturnaddress',
2078 my $countrydefault = $conf->config('countrydefault') || 'US';
2079 if ( $cust_main->country eq $countrydefault ) {
2080 $invoice_data{'country'} = '';
2082 $invoice_data{'country'} =
2083 encode_entities(code2country($cust_main->country));
2087 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2088 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2090 $invoice_data{'notes'} =
2091 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2093 $invoice_data{'notes'} =
2095 s/%%(.*)$/<!-- $1 -->/;
2096 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2097 s/\\begin\{enumerate\}/<ol>/;
2099 s/\\end\{enumerate\}/<\/ol>/;
2100 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2103 $conf->config_orbase('invoice_latexnotes', $template)
2107 # #do variable substitutions in notes
2108 # $invoice_data{'notes'} =
2110 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2111 # $conf->config_orbase('invoice_latexnotes', $suffix)
2115 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2116 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2118 $invoice_data{'footer'} =
2119 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2121 $invoice_data{'footer'} =
2122 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2123 $conf->config_orbase('invoice_latexfooter', $template)
2127 $invoice_data{'po_line'} =
2128 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2129 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2132 my $money_char = $conf->config('money_char') || '$';
2134 foreach my $line_item ( $self->_items ) {
2136 ext_description => [],
2138 $detail->{'ref'} = $line_item->{'pkgnum'};
2139 $detail->{'description'} = encode_entities($line_item->{'description'});
2140 if ( exists $line_item->{'ext_description'} ) {
2141 @{$detail->{'ext_description'}} = map {
2142 encode_entities($_);
2143 } @{$line_item->{'ext_description'}};
2145 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2146 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2148 push @{$invoice_data{'detail_items'}}, $detail;
2153 foreach my $tax ( $self->_items_tax ) {
2155 $total->{'total_item'} = encode_entities($tax->{'description'});
2156 $taxtotal += $tax->{'amount'};
2157 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2158 push @{$invoice_data{'total_items'}}, $total;
2163 $total->{'total_item'} = 'Sub-total';
2164 $total->{'total_amount'} =
2165 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2166 unshift @{$invoice_data{'total_items'}}, $total;
2169 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2172 $total->{'total_item'} = '<b>Total</b>';
2173 $total->{'total_amount'} =
2174 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2175 push @{$invoice_data{'total_items'}}, $total;
2178 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2181 foreach my $credit ( $self->_items_credits ) {
2183 $total->{'total_item'} = encode_entities($credit->{'description'});
2185 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2186 push @{$invoice_data{'total_items'}}, $total;
2190 foreach my $payment ( $self->_items_payments ) {
2192 $total->{'total_item'} = encode_entities($payment->{'description'});
2194 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2195 push @{$invoice_data{'total_items'}}, $total;
2200 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2201 $total->{'total_amount'} =
2202 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2203 push @{$invoice_data{'total_items'}}, $total;
2206 $html_template->fill_in( HASH => \%invoice_data);
2209 # quick subroutine for print_latex
2211 # There are ten characters that LaTeX treats as special characters, which
2212 # means that they do not simply typeset themselves:
2213 # # $ % & ~ _ ^ \ { }
2215 # TeX ignores blanks following an escaped character; if you want a blank (as
2216 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2220 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2221 $value =~ s/([<>])/\$$1\$/g;
2225 #utility methods for print_*
2227 sub balance_due_msg {
2229 my $msg = 'Balance Due';
2230 return $msg unless $conf->exists('invoice_default_terms');
2231 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2232 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2233 } elsif ( $conf->config('invoice_default_terms') ) {
2234 $msg .= ' - '. $conf->config('invoice_default_terms');
2241 my @display = scalar(@_)
2243 : qw( _items_previous _items_pkg );
2244 #: qw( _items_pkg );
2245 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2247 foreach my $display ( @display ) {
2248 push @b, $self->$display(@_);
2253 sub _items_previous {
2255 my $cust_main = $self->cust_main;
2256 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2258 foreach ( @pr_cust_bill ) {
2260 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2261 ' ('. time2str('%x',$_->_date). ')',
2262 #'pkgpart' => 'N/A',
2264 'amount' => sprintf("%.2f", $_->owed),
2270 # 'description' => 'Previous Balance',
2271 # #'pkgpart' => 'N/A',
2272 # 'pkgnum' => 'N/A',
2273 # 'amount' => sprintf("%10.2f", $pr_total ),
2274 # 'ext_description' => [ map {
2275 # "Invoice ". $_->invnum.
2276 # " (". time2str("%x",$_->_date). ") ".
2277 # sprintf("%10.2f", $_->owed)
2278 # } @pr_cust_bill ],
2285 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2286 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2291 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2292 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2295 sub _items_cust_bill_pkg {
2297 my $cust_bill_pkg = shift;
2300 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2302 my $desc = $cust_bill_pkg->desc;
2304 if ( $cust_bill_pkg->pkgnum > 0 ) {
2306 if ( $cust_bill_pkg->setup != 0 ) {
2307 my $description = $desc;
2308 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2309 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2310 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2312 description => $description,
2313 #pkgpart => $part_pkg->pkgpart,
2314 pkgnum => $cust_bill_pkg->pkgnum,
2315 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2316 ext_description => \@d,
2320 if ( $cust_bill_pkg->recur != 0 ) {
2322 description => "$desc (" .
2323 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2324 time2str('%x', $cust_bill_pkg->edate). ')',
2325 #pkgpart => $part_pkg->pkgpart,
2326 pkgnum => $cust_bill_pkg->pkgnum,
2327 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2329 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2330 $cust_bill_pkg->sdate),
2331 $cust_bill_pkg->details,
2336 } else { #pkgnum tax or one-shot line item (??)
2338 if ( $cust_bill_pkg->setup != 0 ) {
2340 'description' => $desc,
2341 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2344 if ( $cust_bill_pkg->recur != 0 ) {
2346 'description' => "$desc (".
2347 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2348 time2str("%x", $cust_bill_pkg->edate). ')',
2349 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2361 sub _items_credits {
2366 foreach ( $self->cust_credited ) {
2368 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2370 my $reason = $_->cust_credit->reason;
2371 #my $reason = substr($_->cust_credit->reason,0,32);
2372 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2373 $reason = " ($reason) " if $reason;
2375 #'description' => 'Credit ref\#'. $_->crednum.
2376 # " (". time2str("%x",$_->cust_credit->_date) .")".
2378 'description' => 'Credit applied '.
2379 time2str("%x",$_->cust_credit->_date). $reason,
2380 'amount' => sprintf("%.2f",$_->amount),
2383 #foreach ( @cr_cust_credit ) {
2385 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2386 # $money_char. sprintf("%10.2f",$_->credited)
2394 sub _items_payments {
2398 #get & print payments
2399 foreach ( $self->cust_bill_pay ) {
2401 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2404 'description' => "Payment received ".
2405 time2str("%x",$_->cust_pay->_date ),
2406 'amount' => sprintf("%.2f", $_->amount )
2424 sub process_reprint {
2425 process_re_X('print', @_);
2432 sub process_reemail {
2433 process_re_X('email', @_);
2441 process_re_X('fax', @_);
2444 use Storable qw(thaw);
2448 my( $method, $job ) = ( shift, shift );
2450 my $param = thaw(decode_base64(shift));
2451 warn Dumper($param) if $DEBUG;
2462 my($method, $job, %param ) = @_;
2463 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2465 #some false laziness w/search/cust_bill.html
2467 my $orderby = 'ORDER BY cust_bill._date';
2471 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2472 push @where, "cust_bill._date >= $1";
2474 if ( $param{'end'} =~ /^(\d+)$/ ) {
2475 push @where, "cust_bill._date < $1";
2477 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2478 push @where, "cust_main.agentnum = $1";
2482 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2483 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2484 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2485 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2487 push @where, "0 != $owed"
2490 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2493 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2495 my $addl_from = 'left join cust_main using ( custnum )';
2497 if ( $param{'newest_percust'} ) {
2498 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2499 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2500 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2503 my @cust_bill = qsearch( 'cust_bill',
2505 "$distinct cust_bill.*",
2511 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2512 foreach my $cust_bill ( @cust_bill ) {
2513 $cust_bill->$method();
2515 if ( $job ) { #progressbar foo
2517 if ( time - $min_sec > $last ) {
2518 my $error = $job->update_statustext(
2519 int( 100 * $num / scalar(@cust_bill) )
2521 die $error if $error;
2536 print_text formatting (and some logic :/) is in source, but needs to be
2537 slurped in from a file. Also number of lines ($=).
2541 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2542 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base