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: $!";
887 =item print_csv OPTION => VALUE, ...
889 Returns CSV data for this invoice.
893 format - 'default' or 'billco'
895 Returns a list consisting of two scalars. The first is a single line of CSV
896 header information for this invoice. The second is one or more lines of CSV
897 detail information for this invoice.
899 If I<format> is not specified or "default", the fields of the CSV file are as
902 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
906 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
908 B<record_type> is C<cust_bill> for the initial header line only. The
909 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
910 fields are filled in.
912 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
913 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
916 =item invnum - invoice number
918 =item custnum - customer number
920 =item _date - invoice date
922 =item charged - total invoice amount
924 =item first - customer first name
926 =item last - customer first name
928 =item company - company name
930 =item address1 - address line 1
932 =item address2 - address line 1
942 =item pkg - line item description
944 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
946 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
948 =item sdate - start date for recurring fee
950 =item edate - end date for recurring fee
954 If I<format> is "billco", the fields of the header CSV file are as follows:
956 +-------------------------------------------------------------------+
957 | FORMAT HEADER FILE |
958 |-------------------------------------------------------------------|
959 | Field | Description | Name | Type | Width |
960 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
961 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
962 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
963 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
964 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
965 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
966 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
967 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
968 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
969 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
970 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
971 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
972 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
973 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
974 | 15 | Previous Balance | BALFWD | NUM* | 9 |
975 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
976 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
977 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
978 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
979 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
980 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
981 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
982 | 23 | Y/N | AGESWITCH | CHAR | 1 |
983 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
984 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
985 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
986 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
987 | 28 | State Tax*** | STATETAX | NUM* | 9 |
988 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
989 +-------+-------------------------------+------------+------+-------+
991 If I<format> is "billco", the fields of the detail CSV file are as follows:
993 FORMAT FOR DETAIL FILE
995 Field | Description | Name | Type | Width
996 1 | N/A-Leave Empty | RC | CHAR | 2
997 2 | N/A-Leave Empty | CUSTID | CHAR | 15
998 3 | Account Number | TRACCTNUM | CHAR | 15
999 4 | Invoice Number | TRINVOICE | CHAR | 15
1000 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1001 6 | Transaction Detail | DETAILS | CHAR | 100
1002 7 | Amount | AMT | NUM* | 9
1003 8 | Line Format Control** | LNCTRL | CHAR | 2
1004 9 | Grouping Code | GROUP | CHAR | 2
1005 10 | User Defined | ACCT CODE | CHAR | 15
1010 my($self, %opt) = @_;
1012 eval "use Text::CSV_XS";
1015 my $cust_main = $self->cust_main;
1017 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1019 if ( lc($opt{'format'}) eq 'billco' ) {
1022 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1025 if ( $conf->exists('invoice_default_terms')
1026 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1027 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1030 my( $previous_balance, @unused ) = $self->previous; #previous balance
1032 my $pmt_cr_applied = 0;
1033 $pmt_cr_applied += $_->{'amount'}
1034 foreach ( $self->_items_payments, $self->_items_credits ) ;
1036 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1039 '', # 1 | N/A-Leave Empty CHAR 2
1040 '', # 2 | N/A-Leave Empty CHAR 15
1041 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1042 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1043 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1044 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1045 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1046 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1047 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1048 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1049 '', # 10 | Ancillary Billing Information CHAR 30
1050 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1051 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1054 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1057 $duedate, # 14 | Bill Due Date CHAR 10
1059 $previous_balance, # 15 | Previous Balance NUM* 9
1060 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1061 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1062 $totaldue, # 18 | Total Amt Due NUM* 9
1063 $totaldue, # 19 | Total Amt Due NUM* 9
1064 '', # 20 | 30 Day Aging NUM* 9
1065 '', # 21 | 60 Day Aging NUM* 9
1066 '', # 22 | 90 Day Aging NUM* 9
1067 'N', # 23 | Y/N CHAR 1
1068 '', # 24 | Remittance automation CHAR 100
1069 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1070 $self->custnum, # 26 | Customer Reference Number CHAR 15
1071 '0', # 27 | Federal Tax*** NUM* 9
1072 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1073 '0', # 29 | Other Taxes & Fees*** NUM* 9
1082 time2str("%x", $self->_date),
1083 sprintf("%.2f", $self->charged),
1084 ( map { $cust_main->getfield($_) }
1085 qw( first last company address1 address2 city state zip country ) ),
1087 ) or die "can't create csv";
1090 my $header = $csv->string. "\n";
1093 if ( lc($opt{'format'}) eq 'billco' ) {
1096 foreach my $item ( $self->_items_pkg ) {
1099 '', # 1 | N/A-Leave Empty CHAR 2
1100 '', # 2 | N/A-Leave Empty CHAR 15
1101 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1102 $self->invnum, # 4 | Invoice Number CHAR 15
1103 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1104 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1105 $item->{'amount'}, # 7 | Amount NUM* 9
1106 '', # 8 | Line Format Control** CHAR 2
1107 '', # 9 | Grouping Code CHAR 2
1108 '', # 10 | User Defined CHAR 15
1111 $detail .= $csv->string. "\n";
1117 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1119 my($pkg, $setup, $recur, $sdate, $edate);
1120 if ( $cust_bill_pkg->pkgnum ) {
1122 ($pkg, $setup, $recur, $sdate, $edate) = (
1123 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1124 ( $cust_bill_pkg->setup != 0
1125 ? sprintf("%.2f", $cust_bill_pkg->setup )
1127 ( $cust_bill_pkg->recur != 0
1128 ? sprintf("%.2f", $cust_bill_pkg->recur )
1130 ( $cust_bill_pkg->sdate
1131 ? time2str("%x", $cust_bill_pkg->sdate)
1133 ($cust_bill_pkg->edate
1134 ?time2str("%x", $cust_bill_pkg->edate)
1138 } else { #pkgnum tax
1139 next unless $cust_bill_pkg->setup != 0;
1140 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1141 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1143 ($pkg, $setup, $recur, $sdate, $edate) =
1144 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1150 ( map { '' } (1..11) ),
1151 ($pkg, $setup, $recur, $sdate, $edate)
1152 ) or die "can't create csv";
1154 $detail .= $csv->string. "\n";
1160 ( $header, $detail );
1166 Pays this invoice with a compliemntary payment. If there is an error,
1167 returns the error, otherwise returns false.
1173 my $cust_pay = new FS::cust_pay ( {
1174 'invnum' => $self->invnum,
1175 'paid' => $self->owed,
1178 'payinfo' => $self->cust_main->payinfo,
1186 Attempts to pay this invoice with a credit card payment via a
1187 Business::OnlinePayment realtime gateway. See
1188 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1189 for supported processors.
1195 $self->realtime_bop( 'CC', @_ );
1200 Attempts to pay this invoice with an electronic check (ACH) payment via a
1201 Business::OnlinePayment realtime gateway. See
1202 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1203 for supported processors.
1209 $self->realtime_bop( 'ECHECK', @_ );
1214 Attempts to pay this invoice with phone bill (LEC) payment via a
1215 Business::OnlinePayment realtime gateway. See
1216 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1217 for supported processors.
1223 $self->realtime_bop( 'LEC', @_ );
1227 my( $self, $method ) = @_;
1229 my $cust_main = $self->cust_main;
1230 my $balance = $cust_main->balance;
1231 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1232 $amount = sprintf("%.2f", $amount);
1233 return "not run (balance $balance)" unless $amount > 0;
1235 my $description = 'Internet Services';
1236 if ( $conf->exists('business-onlinepayment-description') ) {
1237 my $dtempl = $conf->config('business-onlinepayment-description');
1239 my $agent_obj = $cust_main->agent
1240 or die "can't retreive agent for $cust_main (agentnum ".
1241 $cust_main->agentnum. ")";
1242 my $agent = $agent_obj->agent;
1243 my $pkgs = join(', ',
1244 map { $_->cust_pkg->part_pkg->pkg }
1245 grep { $_->pkgnum } $self->cust_bill_pkg
1247 $description = eval qq("$dtempl");
1250 $cust_main->realtime_bop($method, $amount,
1251 'description' => $description,
1252 'invnum' => $self->invnum,
1259 Adds a payment for this invoice to the pending credit card batch (see
1260 L<FS::cust_pay_batch>).
1266 my $cust_main = $self->cust_main;
1268 my $cust_pay_batch = new FS::cust_pay_batch ( {
1269 'invnum' => $self->getfield('invnum'),
1270 'custnum' => $cust_main->getfield('custnum'),
1271 'last' => $cust_main->getfield('last'),
1272 'first' => $cust_main->getfield('first'),
1273 'address1' => $cust_main->getfield('address1'),
1274 'address2' => $cust_main->getfield('address2'),
1275 'city' => $cust_main->getfield('city'),
1276 'state' => $cust_main->getfield('state'),
1277 'zip' => $cust_main->getfield('zip'),
1278 'country' => $cust_main->getfield('country'),
1279 'cardnum' => $cust_main->payinfo,
1280 'exp' => $cust_main->getfield('paydate'),
1281 'payname' => $cust_main->getfield('payname'),
1282 'amount' => $self->owed,
1284 my $error = $cust_pay_batch->insert;
1285 die $error if $error;
1290 sub _agent_template {
1292 $self->_agent_plandata('agent_templatename');
1295 sub _agent_invoice_from {
1297 $self->_agent_plandata('agent_invoice_from');
1300 sub _agent_plandata {
1301 my( $self, $option ) = @_;
1303 my $part_bill_event = qsearchs( 'part_bill_event',
1305 'payby' => $self->cust_main->payby,
1306 'plan' => 'send_agent',
1307 'plandata' => { 'op' => '~',
1308 'value' => "(^|\n)agentnum ".
1310 $self->cust_main->agentnum.
1316 'ORDER BY seconds LIMIT 1'
1319 return '' unless $part_bill_event;
1321 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1324 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1325 " plandata for $option";
1331 =item print_text [ TIME [ , TEMPLATE ] ]
1333 Returns an text invoice, as a list of lines.
1335 TIME an optional value used to control the printing of overdue messages. The
1336 default is now. It isn't the date of the invoice; that's the `_date' field.
1337 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1338 L<Time::Local> and L<Date::Parse> for conversion functions.
1342 #still some false laziness w/_items stuff (and send_csv)
1345 my( $self, $today, $template ) = @_;
1348 # my $invnum = $self->invnum;
1349 my $cust_main = $self->cust_main;
1350 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1351 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1353 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1354 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1355 #my $balance_due = $self->owed + $pr_total - $cr_total;
1356 my $balance_due = $self->owed + $pr_total;
1359 #my($description,$amount);
1363 foreach ( @pr_cust_bill ) {
1365 "Previous Balance, Invoice #". $_->invnum.
1366 " (". time2str("%x",$_->_date). ")",
1367 $money_char. sprintf("%10.2f",$_->owed)
1370 if (@pr_cust_bill) {
1371 push @buf,['','-----------'];
1372 push @buf,[ 'Total Previous Balance',
1373 $money_char. sprintf("%10.2f",$pr_total ) ];
1378 foreach my $cust_bill_pkg (
1379 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1380 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1383 my $desc = $cust_bill_pkg->desc;
1385 if ( $cust_bill_pkg->pkgnum > 0 ) {
1387 if ( $cust_bill_pkg->setup != 0 ) {
1388 my $description = $desc;
1389 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1390 push @buf, [ $description,
1391 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1393 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1394 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1397 if ( $cust_bill_pkg->recur != 0 ) {
1399 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1400 time2str("%x", $cust_bill_pkg->edate) . ")",
1401 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1404 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1405 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1406 $cust_bill_pkg->sdate );
1409 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1411 } else { #pkgnum tax or one-shot line item
1413 if ( $cust_bill_pkg->setup != 0 ) {
1415 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1417 if ( $cust_bill_pkg->recur != 0 ) {
1418 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1419 . time2str("%x", $cust_bill_pkg->edate). ")",
1420 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1428 push @buf,['','-----------'];
1429 push @buf,['Total New Charges',
1430 $money_char. sprintf("%10.2f",$self->charged) ];
1433 push @buf,['','-----------'];
1434 push @buf,['Total Charges',
1435 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1439 foreach ( $self->cust_credited ) {
1441 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1443 my $reason = substr($_->cust_credit->reason,0,32);
1444 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1445 $reason = " ($reason) " if $reason;
1447 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1449 $money_char. sprintf("%10.2f",$_->amount)
1452 #foreach ( @cr_cust_credit ) {
1454 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1455 # $money_char. sprintf("%10.2f",$_->credited)
1459 #get & print payments
1460 foreach ( $self->cust_bill_pay ) {
1462 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1465 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1466 $money_char. sprintf("%10.2f",$_->amount )
1471 my $balance_due_msg = $self->balance_due_msg;
1473 push @buf,['','-----------'];
1474 push @buf,[$balance_due_msg, $money_char.
1475 sprintf("%10.2f", $balance_due ) ];
1477 #create the template
1478 $template ||= $self->_agent_template;
1479 my $templatefile = 'invoice_template';
1480 $templatefile .= "_$template" if length($template);
1481 my @invoice_template = $conf->config($templatefile)
1482 or die "cannot load config file $templatefile";
1485 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1486 /invoice_lines\((\d*)\)/;
1487 $invoice_lines += $1 || scalar(@buf);
1490 die "no invoice_lines() functions in template?" unless $wasfunc;
1491 my $invoice_template = new Text::Template (
1493 SOURCE => [ map "$_\n", @invoice_template ],
1494 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1495 $invoice_template->compile()
1496 or die "can't compile template: $Text::Template::ERROR";
1498 #setup template variables
1499 package FS::cust_bill::_template; #!
1500 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1502 $invnum = $self->invnum;
1503 $date = $self->_date;
1505 $agent = $self->cust_main->agent->agent;
1507 if ( $FS::cust_bill::invoice_lines ) {
1509 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1511 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1516 #format address (variable for the template)
1518 @address = ( '', '', '', '', '', '' );
1519 package FS::cust_bill; #!
1520 $FS::cust_bill::_template::address[$l++] =
1521 $cust_main->payname.
1522 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1523 ? " (P.O. #". $cust_main->payinfo. ")"
1527 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1528 if $cust_main->company;
1529 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1530 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1531 if $cust_main->address2;
1532 $FS::cust_bill::_template::address[$l++] =
1533 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1535 my $countrydefault = $conf->config('countrydefault') || 'US';
1536 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1537 unless $cust_main->country eq $countrydefault;
1539 # #overdue? (variable for the template)
1540 # $FS::cust_bill::_template::overdue = (
1542 # && $today > $self->_date
1543 ## && $self->printed > 1
1544 # && $self->printed > 0
1547 #and subroutine for the template
1548 sub FS::cust_bill::_template::invoice_lines {
1549 my $lines = shift || scalar(@buf);
1551 scalar(@buf) ? shift @buf : [ '', '' ];
1557 $FS::cust_bill::_template::page = 1;
1561 push @collect, split("\n",
1562 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1564 $FS::cust_bill::_template::page++;
1567 map "$_\n", @collect;
1571 =item print_latex [ TIME [ , TEMPLATE ] ]
1573 Internal method - returns a filename of a filled-in LaTeX template for this
1574 invoice (Note: add ".tex" to get the actual filename).
1576 See print_ps and print_pdf for methods that return PostScript and PDF output.
1578 TIME an optional value used to control the printing of overdue messages. The
1579 default is now. It isn't the date of the invoice; that's the `_date' field.
1580 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1581 L<Time::Local> and L<Date::Parse> for conversion functions.
1585 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1588 my( $self, $today, $template ) = @_;
1590 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1593 my $cust_main = $self->cust_main;
1594 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1595 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1597 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1598 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1599 #my $balance_due = $self->owed + $pr_total - $cr_total;
1600 my $balance_due = $self->owed + $pr_total;
1602 #create the template
1603 $template ||= $self->_agent_template;
1604 my $templatefile = 'invoice_latex';
1605 my $suffix = length($template) ? "_$template" : '';
1606 $templatefile .= $suffix;
1607 my @invoice_template = map "$_\n", $conf->config($templatefile)
1608 or die "cannot load config file $templatefile";
1610 my($format, $text_template);
1611 if ( grep { /^%%Detail/ } @invoice_template ) {
1612 #change this to a die when the old code is removed
1613 warn "old-style invoice template $templatefile; ".
1614 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1617 $format = 'Text::Template';
1618 $text_template = new Text::Template(
1620 SOURCE => \@invoice_template,
1621 DELIMITERS => [ '[@--', '--@]' ],
1624 $text_template->compile()
1625 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1629 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1630 $returnaddress = join("\n",
1631 $conf->config_orbase('invoice_latexreturnaddress', $template)
1634 $returnaddress = '~';
1637 my %invoice_data = (
1638 'invnum' => $self->invnum,
1639 'date' => time2str('%b %o, %Y', $self->_date),
1640 'today' => time2str('%b %o, %Y', $today),
1641 'agent' => _latex_escape($cust_main->agent->agent),
1642 'payname' => _latex_escape($cust_main->payname),
1643 'company' => _latex_escape($cust_main->company),
1644 'address1' => _latex_escape($cust_main->address1),
1645 'address2' => _latex_escape($cust_main->address2),
1646 'city' => _latex_escape($cust_main->city),
1647 'state' => _latex_escape($cust_main->state),
1648 'zip' => _latex_escape($cust_main->zip),
1649 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1650 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1651 'returnaddress' => $returnaddress,
1653 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1654 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1655 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1658 my $countrydefault = $conf->config('countrydefault') || 'US';
1659 if ( $cust_main->country eq $countrydefault ) {
1660 $invoice_data{'country'} = '';
1662 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1665 $invoice_data{'notes'} =
1667 # #do variable substitutions in notes
1668 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1669 $conf->config_orbase('invoice_latexnotes', $template)
1671 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1674 $invoice_data{'footer'} =~ s/\n+$//;
1675 $invoice_data{'smallfooter'} =~ s/\n+$//;
1676 $invoice_data{'notes'} =~ s/\n+$//;
1678 $invoice_data{'po_line'} =
1679 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1680 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1684 if ( $format eq 'old' ) {
1687 my @total_item = ();
1688 while ( @invoice_template ) {
1689 my $line = shift @invoice_template;
1691 if ( $line =~ /^%%Detail\s*$/ ) {
1693 while ( ( my $line_item_line = shift @invoice_template )
1694 !~ /^%%EndDetail\s*$/ ) {
1695 push @line_item, $line_item_line;
1697 foreach my $line_item ( $self->_items ) {
1698 #foreach my $line_item ( $self->_items_pkg ) {
1699 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1700 $invoice_data{'description'} =
1701 _latex_escape($line_item->{'description'});
1702 if ( exists $line_item->{'ext_description'} ) {
1703 $invoice_data{'description'} .=
1704 "\\tabularnewline\n~~".
1705 join( "\\tabularnewline\n~~",
1706 map _latex_escape($_), @{$line_item->{'ext_description'}}
1709 $invoice_data{'amount'} = $line_item->{'amount'};
1710 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1712 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1715 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1717 while ( ( my $total_item_line = shift @invoice_template )
1718 !~ /^%%EndTotalDetails\s*$/ ) {
1719 push @total_item, $total_item_line;
1722 my @total_fill = ();
1725 foreach my $tax ( $self->_items_tax ) {
1726 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1727 $taxtotal += $tax->{'amount'};
1728 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1730 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1735 $invoice_data{'total_item'} = 'Sub-total';
1736 $invoice_data{'total_amount'} =
1737 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1738 unshift @total_fill,
1739 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1743 $invoice_data{'total_item'} = '\textbf{Total}';
1744 $invoice_data{'total_amount'} =
1745 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1747 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1750 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1753 foreach my $credit ( $self->_items_credits ) {
1754 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1756 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1758 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1763 foreach my $payment ( $self->_items_payments ) {
1764 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1766 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1768 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1772 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1773 $invoice_data{'total_amount'} =
1774 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1776 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1779 push @filled_in, @total_fill;
1782 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1783 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1784 push @filled_in, $line;
1795 } elsif ( $format eq 'Text::Template' ) {
1797 my @detail_items = ();
1798 my @total_items = ();
1800 $invoice_data{'detail_items'} = \@detail_items;
1801 $invoice_data{'total_items'} = \@total_items;
1803 foreach my $line_item ( $self->_items ) {
1805 ext_description => [],
1807 $detail->{'ref'} = $line_item->{'pkgnum'};
1808 $detail->{'quantity'} = 1;
1809 $detail->{'description'} = _latex_escape($line_item->{'description'});
1810 if ( exists $line_item->{'ext_description'} ) {
1811 @{$detail->{'ext_description'}} = map {
1813 } @{$line_item->{'ext_description'}};
1815 $detail->{'amount'} = $line_item->{'amount'};
1816 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1818 push @detail_items, $detail;
1823 foreach my $tax ( $self->_items_tax ) {
1825 $total->{'total_item'} = _latex_escape($tax->{'description'});
1826 $taxtotal += $tax->{'amount'};
1827 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1828 push @total_items, $total;
1833 $total->{'total_item'} = 'Sub-total';
1834 $total->{'total_amount'} =
1835 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1836 unshift @total_items, $total;
1841 $total->{'total_item'} = '\textbf{Total}';
1842 $total->{'total_amount'} =
1843 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1844 push @total_items, $total;
1847 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1850 foreach my $credit ( $self->_items_credits ) {
1852 $total->{'total_item'} = _latex_escape($credit->{'description'});
1854 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1855 push @total_items, $total;
1859 foreach my $payment ( $self->_items_payments ) {
1861 $total->{'total_item'} = _latex_escape($payment->{'description'});
1863 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1864 push @total_items, $total;
1869 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1870 $total->{'total_amount'} =
1871 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1872 push @total_items, $total;
1876 die "guru meditation #54";
1879 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1880 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1884 ) or die "can't open temp file: $!\n";
1885 if ( $format eq 'old' ) {
1886 print $fh join('', @filled_in );
1887 } elsif ( $format eq 'Text::Template' ) {
1888 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1890 die "guru meditation #32";
1894 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1899 =item print_ps [ TIME [ , TEMPLATE ] ]
1901 Returns an postscript invoice, as a scalar.
1903 TIME an optional value used to control the printing of overdue messages. The
1904 default is now. It isn't the date of the invoice; that's the `_date' field.
1905 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1906 L<Time::Local> and L<Date::Parse> for conversion functions.
1913 my $file = $self->print_latex(@_);
1915 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1918 my $sfile = shell_quote $file;
1920 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1921 or die "pslatex $file.tex failed; see $file.log for details?\n";
1922 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1923 or die "pslatex $file.tex failed; see $file.log for details?\n";
1925 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1926 or die "dvips failed";
1928 open(POSTSCRIPT, "<$file.ps")
1929 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1931 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1934 while (<POSTSCRIPT>) {
1944 =item print_pdf [ TIME [ , TEMPLATE ] ]
1946 Returns an PDF invoice, as a scalar.
1948 TIME an optional value used to control the printing of overdue messages. The
1949 default is now. It isn't the date of the invoice; that's the `_date' field.
1950 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1951 L<Time::Local> and L<Date::Parse> for conversion functions.
1958 my $file = $self->print_latex(@_);
1960 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1963 #system('pdflatex', "$file.tex");
1964 #system('pdflatex', "$file.tex");
1965 #! LaTeX Error: Unknown graphics extension: .eps.
1967 my $sfile = shell_quote $file;
1969 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1970 or die "pslatex $file.tex failed; see $file.log for details?\n";
1971 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1972 or die "pslatex $file.tex failed; see $file.log for details?\n";
1974 #system('dvipdf', "$file.dvi", "$file.pdf" );
1976 "dvips -q -t letter -f $sfile.dvi ".
1977 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1980 or die "dvips | gs failed: $!";
1982 open(PDF, "<$file.pdf")
1983 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1985 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1998 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2000 Returns an HTML invoice, as a scalar.
2002 TIME an optional value used to control the printing of overdue messages. The
2003 default is now. It isn't the date of the invoice; that's the `_date' field.
2004 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2005 L<Time::Local> and L<Date::Parse> for conversion functions.
2007 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2008 when emailing the invoice as part of a multipart/related MIME email.
2012 #some falze laziness w/print_text and print_latex (and send_csv)
2014 my( $self, $today, $template, $cid ) = @_;
2017 my $cust_main = $self->cust_main;
2018 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2019 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2021 $template ||= $self->_agent_template;
2022 my $templatefile = 'invoice_html';
2023 my $suffix = length($template) ? "_$template" : '';
2024 $templatefile .= $suffix;
2025 my @html_template = map "$_\n", $conf->config($templatefile)
2026 or die "cannot load config file $templatefile";
2028 my $html_template = new Text::Template(
2030 SOURCE => \@html_template,
2031 DELIMITERS => [ '<%=', '%>' ],
2034 $html_template->compile()
2035 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2037 my %invoice_data = (
2038 'invnum' => $self->invnum,
2039 'date' => time2str('%b %o, %Y', $self->_date),
2040 'today' => time2str('%b %o, %Y', $today),
2041 'agent' => encode_entities($cust_main->agent->agent),
2042 'payname' => encode_entities($cust_main->payname),
2043 'company' => encode_entities($cust_main->company),
2044 'address1' => encode_entities($cust_main->address1),
2045 'address2' => encode_entities($cust_main->address2),
2046 'city' => encode_entities($cust_main->city),
2047 'state' => encode_entities($cust_main->state),
2048 'zip' => encode_entities($cust_main->zip),
2049 'terms' => $conf->config('invoice_default_terms')
2050 || 'Payable upon receipt',
2052 'template' => $template,
2053 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2057 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2058 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2060 $invoice_data{'returnaddress'} =
2061 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2063 $invoice_data{'returnaddress'} =
2066 s/\\\\\*?\s*$/<BR>/;
2067 s/\\hyphenation\{[\w\s\-]+\}//;
2070 $conf->config_orbase( 'invoice_latexreturnaddress',
2076 my $countrydefault = $conf->config('countrydefault') || 'US';
2077 if ( $cust_main->country eq $countrydefault ) {
2078 $invoice_data{'country'} = '';
2080 $invoice_data{'country'} =
2081 encode_entities(code2country($cust_main->country));
2085 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2086 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2088 $invoice_data{'notes'} =
2089 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2091 $invoice_data{'notes'} =
2093 s/%%(.*)$/<!-- $1 -->/;
2094 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2095 s/\\begin\{enumerate\}/<ol>/;
2097 s/\\end\{enumerate\}/<\/ol>/;
2098 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2101 $conf->config_orbase('invoice_latexnotes', $template)
2105 # #do variable substitutions in notes
2106 # $invoice_data{'notes'} =
2108 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2109 # $conf->config_orbase('invoice_latexnotes', $suffix)
2113 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2114 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2116 $invoice_data{'footer'} =
2117 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2119 $invoice_data{'footer'} =
2120 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2121 $conf->config_orbase('invoice_latexfooter', $template)
2125 $invoice_data{'po_line'} =
2126 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2127 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2130 my $money_char = $conf->config('money_char') || '$';
2132 foreach my $line_item ( $self->_items ) {
2134 ext_description => [],
2136 $detail->{'ref'} = $line_item->{'pkgnum'};
2137 $detail->{'description'} = encode_entities($line_item->{'description'});
2138 if ( exists $line_item->{'ext_description'} ) {
2139 @{$detail->{'ext_description'}} = map {
2140 encode_entities($_);
2141 } @{$line_item->{'ext_description'}};
2143 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2144 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2146 push @{$invoice_data{'detail_items'}}, $detail;
2151 foreach my $tax ( $self->_items_tax ) {
2153 $total->{'total_item'} = encode_entities($tax->{'description'});
2154 $taxtotal += $tax->{'amount'};
2155 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2156 push @{$invoice_data{'total_items'}}, $total;
2161 $total->{'total_item'} = 'Sub-total';
2162 $total->{'total_amount'} =
2163 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2164 unshift @{$invoice_data{'total_items'}}, $total;
2167 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2170 $total->{'total_item'} = '<b>Total</b>';
2171 $total->{'total_amount'} =
2172 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2173 push @{$invoice_data{'total_items'}}, $total;
2176 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2179 foreach my $credit ( $self->_items_credits ) {
2181 $total->{'total_item'} = encode_entities($credit->{'description'});
2183 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2184 push @{$invoice_data{'total_items'}}, $total;
2188 foreach my $payment ( $self->_items_payments ) {
2190 $total->{'total_item'} = encode_entities($payment->{'description'});
2192 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2193 push @{$invoice_data{'total_items'}}, $total;
2198 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2199 $total->{'total_amount'} =
2200 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2201 push @{$invoice_data{'total_items'}}, $total;
2204 $html_template->fill_in( HASH => \%invoice_data);
2207 # quick subroutine for print_latex
2209 # There are ten characters that LaTeX treats as special characters, which
2210 # means that they do not simply typeset themselves:
2211 # # $ % & ~ _ ^ \ { }
2213 # TeX ignores blanks following an escaped character; if you want a blank (as
2214 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2218 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2219 $value =~ s/([<>])/\$$1\$/g;
2223 #utility methods for print_*
2225 sub balance_due_msg {
2227 my $msg = 'Balance Due';
2228 return $msg unless $conf->exists('invoice_default_terms');
2229 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2230 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2231 } elsif ( $conf->config('invoice_default_terms') ) {
2232 $msg .= ' - '. $conf->config('invoice_default_terms');
2239 my @display = scalar(@_)
2241 : qw( _items_previous _items_pkg );
2242 #: qw( _items_pkg );
2243 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2245 foreach my $display ( @display ) {
2246 push @b, $self->$display(@_);
2251 sub _items_previous {
2253 my $cust_main = $self->cust_main;
2254 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2256 foreach ( @pr_cust_bill ) {
2258 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2259 ' ('. time2str('%x',$_->_date). ')',
2260 #'pkgpart' => 'N/A',
2262 'amount' => sprintf("%.2f", $_->owed),
2268 # 'description' => 'Previous Balance',
2269 # #'pkgpart' => 'N/A',
2270 # 'pkgnum' => 'N/A',
2271 # 'amount' => sprintf("%10.2f", $pr_total ),
2272 # 'ext_description' => [ map {
2273 # "Invoice ". $_->invnum.
2274 # " (". time2str("%x",$_->_date). ") ".
2275 # sprintf("%10.2f", $_->owed)
2276 # } @pr_cust_bill ],
2283 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2284 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2289 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2290 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2293 sub _items_cust_bill_pkg {
2295 my $cust_bill_pkg = shift;
2298 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2300 my $desc = $cust_bill_pkg->desc;
2302 if ( $cust_bill_pkg->pkgnum > 0 ) {
2304 if ( $cust_bill_pkg->setup != 0 ) {
2305 my $description = $desc;
2306 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2307 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2308 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2310 description => $description,
2311 #pkgpart => $part_pkg->pkgpart,
2312 pkgnum => $cust_bill_pkg->pkgnum,
2313 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2314 ext_description => \@d,
2318 if ( $cust_bill_pkg->recur != 0 ) {
2320 description => "$desc (" .
2321 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2322 time2str('%x', $cust_bill_pkg->edate). ')',
2323 #pkgpart => $part_pkg->pkgpart,
2324 pkgnum => $cust_bill_pkg->pkgnum,
2325 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2327 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2328 $cust_bill_pkg->sdate),
2329 $cust_bill_pkg->details,
2334 } else { #pkgnum tax or one-shot line item (??)
2336 if ( $cust_bill_pkg->setup != 0 ) {
2338 'description' => $desc,
2339 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2342 if ( $cust_bill_pkg->recur != 0 ) {
2344 'description' => "$desc (".
2345 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2346 time2str("%x", $cust_bill_pkg->edate). ')',
2347 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2359 sub _items_credits {
2364 foreach ( $self->cust_credited ) {
2366 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2368 my $reason = $_->cust_credit->reason;
2369 #my $reason = substr($_->cust_credit->reason,0,32);
2370 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2371 $reason = " ($reason) " if $reason;
2373 #'description' => 'Credit ref\#'. $_->crednum.
2374 # " (". time2str("%x",$_->cust_credit->_date) .")".
2376 'description' => 'Credit applied '.
2377 time2str("%x",$_->cust_credit->_date). $reason,
2378 'amount' => sprintf("%.2f",$_->amount),
2381 #foreach ( @cr_cust_credit ) {
2383 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2384 # $money_char. sprintf("%10.2f",$_->credited)
2392 sub _items_payments {
2396 #get & print payments
2397 foreach ( $self->cust_bill_pay ) {
2399 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2402 'description' => "Payment received ".
2403 time2str("%x",$_->cust_pay->_date ),
2404 'amount' => sprintf("%.2f", $_->amount )
2422 sub process_reprint {
2423 process_re_X('print', @_);
2430 sub process_reemail {
2431 process_re_X('email', @_);
2439 process_re_X('fax', @_);
2442 use Storable qw(thaw);
2446 my( $method, $job ) = ( shift, shift );
2448 my $param = thaw(decode_base64(shift));
2449 warn Dumper($param) if $DEBUG;
2460 my($method, $job, %param ) = @_;
2461 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2463 #some false laziness w/search/cust_bill.html
2465 my $orderby = 'ORDER BY cust_bill._date';
2469 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2470 push @where, "cust_bill._date >= $1";
2472 if ( $param{'end'} =~ /^(\d+)$/ ) {
2473 push @where, "cust_bill._date < $1";
2475 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2476 push @where, "cust_main.agentnum = $1";
2480 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2481 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2482 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2483 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2485 push @where, "0 != $owed"
2488 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2491 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2493 my $addl_from = 'left join cust_main using ( custnum )';
2495 if ( $param{'newest_percust'} ) {
2496 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2497 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2498 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2501 my @cust_bill = qsearch( 'cust_bill',
2503 "$distinct cust_bill.*",
2509 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2510 foreach my $cust_bill ( @cust_bill ) {
2511 $cust_bill->$method();
2513 if ( $job ) { #progressbar foo
2515 if ( time - $min_sec > $last ) {
2516 my $error = $job->update_statustext(
2517 int( 100 * $num / scalar(@cust_bill) )
2519 die $error if $error;
2534 print_text formatting (and some logic :/) is in source, but needs to be
2535 slurped in from a file. Also number of lines ($=).
2539 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2540 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base