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.
825 format - 'default' or 'billco'
830 my($self, %opt) = @_;
834 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
835 mkdir $spooldir, 0700 unless -d $spooldir;
837 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
838 my $file = "$spooldir/spool";
839 if ( lc($opt{'format'}) eq 'billco' ) {
840 $file .= '-header.csv';
842 #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
846 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
848 open(CSV, ">>$file") or die "can't open $file: $!";
855 if ( lc($opt{'format'}) eq 'billco' ) {
861 $file = "$spooldir/spool-detail.csv";
863 open(CSV,">>$file") or die "can't open $file: $!";
875 =item print_csv OPTION => VALUE, ...
877 Returns CSV data for this invoice.
881 format - 'default' or 'billco'
883 Returns a list consisting of two scalars. The first is a single line of CSV
884 header information for this invoice. The second is one or more lines of CSV
885 detail information for this invoice.
887 If I<format> is not specified or "default", the fields of the CSV file are as
890 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
894 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
896 B<record_type> is C<cust_bill> for the initial header line only. The
897 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
898 fields are filled in.
900 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
901 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
904 =item invnum - invoice number
906 =item custnum - customer number
908 =item _date - invoice date
910 =item charged - total invoice amount
912 =item first - customer first name
914 =item last - customer first name
916 =item company - company name
918 =item address1 - address line 1
920 =item address2 - address line 1
930 =item pkg - line item description
932 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
934 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
936 =item sdate - start date for recurring fee
938 =item edate - end date for recurring fee
942 If I<format> is "billco", the fields of the header CSV file are as follows:
944 +-------------------------------------------------------------------+
945 | FORMAT HEADER FILE |
946 |-------------------------------------------------------------------|
947 | Field | Description | Name | Type | Width |
948 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
949 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
950 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
951 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
952 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
953 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
954 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
955 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
956 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
957 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
958 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
959 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
960 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
961 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
962 | 15 | Previous Balance | BALFWD | NUM* | 9 |
963 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
964 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
965 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
966 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
967 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
968 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
969 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
970 | 23 | Y/N | AGESWITCH | CHAR | 1 |
971 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
972 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
973 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
974 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
975 | 28 | State Tax*** | STATETAX | NUM* | 9 |
976 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
977 +-------+-------------------------------+------------+------+-------+
979 If I<format> is "billco", the fields of the detail CSV file are as follows:
981 FORMAT FOR DETAIL FILE
983 Field | Description | Name | Type | Width
984 1 | N/A-Leave Empty | RC | CHAR | 2
985 2 | N/A-Leave Empty | CUSTID | CHAR | 15
986 3 | Account Number | TRACCTNUM | CHAR | 15
987 4 | Invoice Number | TRINVOICE | CHAR | 15
988 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
989 6 | Transaction Detail | DETAILS | CHAR | 100
990 7 | Amount | AMT | NUM* | 9
991 8 | Line Format Control** | LNCTRL | CHAR | 2
992 9 | Grouping Code | GROUP | CHAR | 2
993 10 | User Defined | ACCT CODE | CHAR | 15
998 my($self, %opt) = @_;
1000 eval "use Text::CSV_XS";
1003 my $cust_main = $self->cust_main;
1005 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1007 if ( lc($opt{'format'}) eq 'billco' ) {
1010 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1013 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1014 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1017 my( $previous_balance, @unused ) = $self->previous; #previous balance
1019 my $pmt_cr_applied = 0;
1020 $pmt_cr_applied += $_->{'amount'}
1021 foreach ( $self->_items_payments, $self->_items_credits ) ;
1023 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1026 '', # 1 | N/A-Leave Empty CHAR 2
1027 '', # 2 | N/A-Leave Empty CHAR 15
1028 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1029 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1030 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1031 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1032 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1033 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1034 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1035 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1036 '', # 10 | Ancillary Billing Information CHAR 30
1037 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1038 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1041 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1044 $duedate, # 14 | Bill Due Date CHAR 10
1046 $previous_balance, # 15 | Previous Balance NUM* 9
1047 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1048 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1049 $totaldue, # 18 | Total Amt Due NUM* 9
1050 $totaldue, # 19 | Total Amt Due NUM* 9
1051 '', # 20 | 30 Day Aging NUM* 9
1052 '', # 21 | 60 Day Aging NUM* 9
1053 '', # 22 | 90 Day Aging NUM* 9
1054 'N', # 23 | Y/N CHAR 1
1055 '', # 24 | Remittance automation CHAR 100
1056 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1057 $self->custnum, # 26 | Customer Reference Number CHAR 15
1058 '0', # 27 | Federal Tax*** NUM* 9
1059 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1060 '0', # 29 | Other Taxes & Fees*** NUM* 9
1069 time2str("%x", $self->_date),
1070 sprintf("%.2f", $self->charged),
1071 ( map { $cust_main->getfield($_) }
1072 qw( first last company address1 address2 city state zip country ) ),
1074 ) or die "can't create csv";
1077 my $header = $csv->string. "\n";
1080 if ( lc($opt{'format'}) eq 'billco' ) {
1083 foreach my $item ( $self->_items_pkg ) {
1086 '', # 1 | N/A-Leave Empty CHAR 2
1087 '', # 2 | N/A-Leave Empty CHAR 15
1088 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1089 $self->invnum, # 4 | Invoice Number CHAR 15
1090 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1091 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1092 $item->{'amount'}, # 7 | Amount NUM* 9
1093 '', # 8 | Line Format Control** CHAR 2
1094 '', # 9 | Grouping Code CHAR 2
1095 '', # 10 | User Defined CHAR 15
1098 $detail .= $csv->string. "\n";
1104 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1106 my($pkg, $setup, $recur, $sdate, $edate);
1107 if ( $cust_bill_pkg->pkgnum ) {
1109 ($pkg, $setup, $recur, $sdate, $edate) = (
1110 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1111 ( $cust_bill_pkg->setup != 0
1112 ? sprintf("%.2f", $cust_bill_pkg->setup )
1114 ( $cust_bill_pkg->recur != 0
1115 ? sprintf("%.2f", $cust_bill_pkg->recur )
1117 ( $cust_bill_pkg->sdate
1118 ? time2str("%x", $cust_bill_pkg->sdate)
1120 ($cust_bill_pkg->edate
1121 ?time2str("%x", $cust_bill_pkg->edate)
1125 } else { #pkgnum tax
1126 next unless $cust_bill_pkg->setup != 0;
1127 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1128 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1130 ($pkg, $setup, $recur, $sdate, $edate) =
1131 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1137 ( map { '' } (1..11) ),
1138 ($pkg, $setup, $recur, $sdate, $edate)
1139 ) or die "can't create csv";
1141 $detail .= $csv->string. "\n";
1147 ( $header, $detail );
1153 Pays this invoice with a compliemntary payment. If there is an error,
1154 returns the error, otherwise returns false.
1160 my $cust_pay = new FS::cust_pay ( {
1161 'invnum' => $self->invnum,
1162 'paid' => $self->owed,
1165 'payinfo' => $self->cust_main->payinfo,
1173 Attempts to pay this invoice with a credit card payment via a
1174 Business::OnlinePayment realtime gateway. See
1175 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1176 for supported processors.
1182 $self->realtime_bop( 'CC', @_ );
1187 Attempts to pay this invoice with an electronic check (ACH) payment via a
1188 Business::OnlinePayment realtime gateway. See
1189 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1190 for supported processors.
1196 $self->realtime_bop( 'ECHECK', @_ );
1201 Attempts to pay this invoice with phone bill (LEC) payment via a
1202 Business::OnlinePayment realtime gateway. See
1203 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1204 for supported processors.
1210 $self->realtime_bop( 'LEC', @_ );
1214 my( $self, $method ) = @_;
1216 my $cust_main = $self->cust_main;
1217 my $balance = $cust_main->balance;
1218 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1219 $amount = sprintf("%.2f", $amount);
1220 return "not run (balance $balance)" unless $amount > 0;
1222 my $description = 'Internet Services';
1223 if ( $conf->exists('business-onlinepayment-description') ) {
1224 my $dtempl = $conf->config('business-onlinepayment-description');
1226 my $agent_obj = $cust_main->agent
1227 or die "can't retreive agent for $cust_main (agentnum ".
1228 $cust_main->agentnum. ")";
1229 my $agent = $agent_obj->agent;
1230 my $pkgs = join(', ',
1231 map { $_->cust_pkg->part_pkg->pkg }
1232 grep { $_->pkgnum } $self->cust_bill_pkg
1234 $description = eval qq("$dtempl");
1237 $cust_main->realtime_bop($method, $amount,
1238 'description' => $description,
1239 'invnum' => $self->invnum,
1246 Adds a payment for this invoice to the pending credit card batch (see
1247 L<FS::cust_pay_batch>).
1253 my $cust_main = $self->cust_main;
1255 my $cust_pay_batch = new FS::cust_pay_batch ( {
1256 'invnum' => $self->getfield('invnum'),
1257 'custnum' => $cust_main->getfield('custnum'),
1258 'last' => $cust_main->getfield('last'),
1259 'first' => $cust_main->getfield('first'),
1260 'address1' => $cust_main->getfield('address1'),
1261 'address2' => $cust_main->getfield('address2'),
1262 'city' => $cust_main->getfield('city'),
1263 'state' => $cust_main->getfield('state'),
1264 'zip' => $cust_main->getfield('zip'),
1265 'country' => $cust_main->getfield('country'),
1266 'cardnum' => $cust_main->payinfo,
1267 'exp' => $cust_main->getfield('paydate'),
1268 'payname' => $cust_main->getfield('payname'),
1269 'amount' => $self->owed,
1271 my $error = $cust_pay_batch->insert;
1272 die $error if $error;
1277 sub _agent_template {
1279 $self->_agent_plandata('agent_templatename');
1282 sub _agent_invoice_from {
1284 $self->_agent_plandata('agent_invoice_from');
1287 sub _agent_plandata {
1288 my( $self, $option ) = @_;
1290 my $part_bill_event = qsearchs( 'part_bill_event',
1292 'payby' => $self->cust_main->payby,
1293 'plan' => 'send_agent',
1294 'plandata' => { 'op' => '~',
1295 'value' => "(^|\n)agentnum ".
1297 $self->cust_main->agentnum.
1303 'ORDER BY seconds LIMIT 1'
1306 return '' unless $part_bill_event;
1308 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1311 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1312 " plandata for $option";
1318 =item print_text [ TIME [ , TEMPLATE ] ]
1320 Returns an text invoice, as a list of lines.
1322 TIME an optional value used to control the printing of overdue messages. The
1323 default is now. It isn't the date of the invoice; that's the `_date' field.
1324 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1325 L<Time::Local> and L<Date::Parse> for conversion functions.
1329 #still some false laziness w/_items stuff (and send_csv)
1332 my( $self, $today, $template ) = @_;
1335 # my $invnum = $self->invnum;
1336 my $cust_main = $self->cust_main;
1337 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1338 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1340 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1341 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1342 #my $balance_due = $self->owed + $pr_total - $cr_total;
1343 my $balance_due = $self->owed + $pr_total;
1346 #my($description,$amount);
1350 foreach ( @pr_cust_bill ) {
1352 "Previous Balance, Invoice #". $_->invnum.
1353 " (". time2str("%x",$_->_date). ")",
1354 $money_char. sprintf("%10.2f",$_->owed)
1357 if (@pr_cust_bill) {
1358 push @buf,['','-----------'];
1359 push @buf,[ 'Total Previous Balance',
1360 $money_char. sprintf("%10.2f",$pr_total ) ];
1365 foreach my $cust_bill_pkg (
1366 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1367 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1370 my $desc = $cust_bill_pkg->desc;
1372 if ( $cust_bill_pkg->pkgnum > 0 ) {
1374 if ( $cust_bill_pkg->setup != 0 ) {
1375 my $description = $desc;
1376 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1377 push @buf, [ $description,
1378 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1380 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1381 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1384 if ( $cust_bill_pkg->recur != 0 ) {
1386 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1387 time2str("%x", $cust_bill_pkg->edate) . ")",
1388 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1391 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1392 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1393 $cust_bill_pkg->sdate );
1396 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1398 } else { #pkgnum tax or one-shot line item
1400 if ( $cust_bill_pkg->setup != 0 ) {
1402 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1404 if ( $cust_bill_pkg->recur != 0 ) {
1405 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1406 . time2str("%x", $cust_bill_pkg->edate). ")",
1407 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1415 push @buf,['','-----------'];
1416 push @buf,['Total New Charges',
1417 $money_char. sprintf("%10.2f",$self->charged) ];
1420 push @buf,['','-----------'];
1421 push @buf,['Total Charges',
1422 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1426 foreach ( $self->cust_credited ) {
1428 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1430 my $reason = substr($_->cust_credit->reason,0,32);
1431 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1432 $reason = " ($reason) " if $reason;
1434 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1436 $money_char. sprintf("%10.2f",$_->amount)
1439 #foreach ( @cr_cust_credit ) {
1441 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1442 # $money_char. sprintf("%10.2f",$_->credited)
1446 #get & print payments
1447 foreach ( $self->cust_bill_pay ) {
1449 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1452 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1453 $money_char. sprintf("%10.2f",$_->amount )
1458 my $balance_due_msg = $self->balance_due_msg;
1460 push @buf,['','-----------'];
1461 push @buf,[$balance_due_msg, $money_char.
1462 sprintf("%10.2f", $balance_due ) ];
1464 #create the template
1465 $template ||= $self->_agent_template;
1466 my $templatefile = 'invoice_template';
1467 $templatefile .= "_$template" if length($template);
1468 my @invoice_template = $conf->config($templatefile)
1469 or die "cannot load config file $templatefile";
1472 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1473 /invoice_lines\((\d*)\)/;
1474 $invoice_lines += $1 || scalar(@buf);
1477 die "no invoice_lines() functions in template?" unless $wasfunc;
1478 my $invoice_template = new Text::Template (
1480 SOURCE => [ map "$_\n", @invoice_template ],
1481 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1482 $invoice_template->compile()
1483 or die "can't compile template: $Text::Template::ERROR";
1485 #setup template variables
1486 package FS::cust_bill::_template; #!
1487 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1489 $invnum = $self->invnum;
1490 $date = $self->_date;
1492 $agent = $self->cust_main->agent->agent;
1494 if ( $FS::cust_bill::invoice_lines ) {
1496 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1498 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1503 #format address (variable for the template)
1505 @address = ( '', '', '', '', '', '' );
1506 package FS::cust_bill; #!
1507 $FS::cust_bill::_template::address[$l++] =
1508 $cust_main->payname.
1509 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1510 ? " (P.O. #". $cust_main->payinfo. ")"
1514 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1515 if $cust_main->company;
1516 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1517 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1518 if $cust_main->address2;
1519 $FS::cust_bill::_template::address[$l++] =
1520 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1522 my $countrydefault = $conf->config('countrydefault') || 'US';
1523 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1524 unless $cust_main->country eq $countrydefault;
1526 # #overdue? (variable for the template)
1527 # $FS::cust_bill::_template::overdue = (
1529 # && $today > $self->_date
1530 ## && $self->printed > 1
1531 # && $self->printed > 0
1534 #and subroutine for the template
1535 sub FS::cust_bill::_template::invoice_lines {
1536 my $lines = shift || scalar(@buf);
1538 scalar(@buf) ? shift @buf : [ '', '' ];
1544 $FS::cust_bill::_template::page = 1;
1548 push @collect, split("\n",
1549 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1551 $FS::cust_bill::_template::page++;
1554 map "$_\n", @collect;
1558 =item print_latex [ TIME [ , TEMPLATE ] ]
1560 Internal method - returns a filename of a filled-in LaTeX template for this
1561 invoice (Note: add ".tex" to get the actual filename).
1563 See print_ps and print_pdf for methods that return PostScript and PDF output.
1565 TIME an optional value used to control the printing of overdue messages. The
1566 default is now. It isn't the date of the invoice; that's the `_date' field.
1567 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1568 L<Time::Local> and L<Date::Parse> for conversion functions.
1572 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1575 my( $self, $today, $template ) = @_;
1577 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1580 my $cust_main = $self->cust_main;
1581 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1582 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1584 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1585 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1586 #my $balance_due = $self->owed + $pr_total - $cr_total;
1587 my $balance_due = $self->owed + $pr_total;
1589 #create the template
1590 $template ||= $self->_agent_template;
1591 my $templatefile = 'invoice_latex';
1592 my $suffix = length($template) ? "_$template" : '';
1593 $templatefile .= $suffix;
1594 my @invoice_template = map "$_\n", $conf->config($templatefile)
1595 or die "cannot load config file $templatefile";
1597 my($format, $text_template);
1598 if ( grep { /^%%Detail/ } @invoice_template ) {
1599 #change this to a die when the old code is removed
1600 warn "old-style invoice template $templatefile; ".
1601 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1604 $format = 'Text::Template';
1605 $text_template = new Text::Template(
1607 SOURCE => \@invoice_template,
1608 DELIMITERS => [ '[@--', '--@]' ],
1611 $text_template->compile()
1612 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1616 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1617 $returnaddress = join("\n",
1618 $conf->config_orbase('invoice_latexreturnaddress', $template)
1621 $returnaddress = '~';
1624 my %invoice_data = (
1625 'invnum' => $self->invnum,
1626 'date' => time2str('%b %o, %Y', $self->_date),
1627 'today' => time2str('%b %o, %Y', $today),
1628 'agent' => _latex_escape($cust_main->agent->agent),
1629 'payname' => _latex_escape($cust_main->payname),
1630 'company' => _latex_escape($cust_main->company),
1631 'address1' => _latex_escape($cust_main->address1),
1632 'address2' => _latex_escape($cust_main->address2),
1633 'city' => _latex_escape($cust_main->city),
1634 'state' => _latex_escape($cust_main->state),
1635 'zip' => _latex_escape($cust_main->zip),
1636 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1637 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1638 'returnaddress' => $returnaddress,
1640 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1641 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1642 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1645 my $countrydefault = $conf->config('countrydefault') || 'US';
1646 if ( $cust_main->country eq $countrydefault ) {
1647 $invoice_data{'country'} = '';
1649 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1652 $invoice_data{'notes'} =
1654 # #do variable substitutions in notes
1655 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1656 $conf->config_orbase('invoice_latexnotes', $template)
1658 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1661 $invoice_data{'footer'} =~ s/\n+$//;
1662 $invoice_data{'smallfooter'} =~ s/\n+$//;
1663 $invoice_data{'notes'} =~ s/\n+$//;
1665 $invoice_data{'po_line'} =
1666 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1667 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1671 if ( $format eq 'old' ) {
1674 my @total_item = ();
1675 while ( @invoice_template ) {
1676 my $line = shift @invoice_template;
1678 if ( $line =~ /^%%Detail\s*$/ ) {
1680 while ( ( my $line_item_line = shift @invoice_template )
1681 !~ /^%%EndDetail\s*$/ ) {
1682 push @line_item, $line_item_line;
1684 foreach my $line_item ( $self->_items ) {
1685 #foreach my $line_item ( $self->_items_pkg ) {
1686 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1687 $invoice_data{'description'} =
1688 _latex_escape($line_item->{'description'});
1689 if ( exists $line_item->{'ext_description'} ) {
1690 $invoice_data{'description'} .=
1691 "\\tabularnewline\n~~".
1692 join( "\\tabularnewline\n~~",
1693 map _latex_escape($_), @{$line_item->{'ext_description'}}
1696 $invoice_data{'amount'} = $line_item->{'amount'};
1697 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1699 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1702 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1704 while ( ( my $total_item_line = shift @invoice_template )
1705 !~ /^%%EndTotalDetails\s*$/ ) {
1706 push @total_item, $total_item_line;
1709 my @total_fill = ();
1712 foreach my $tax ( $self->_items_tax ) {
1713 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1714 $taxtotal += $tax->{'amount'};
1715 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1717 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1722 $invoice_data{'total_item'} = 'Sub-total';
1723 $invoice_data{'total_amount'} =
1724 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1725 unshift @total_fill,
1726 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1730 $invoice_data{'total_item'} = '\textbf{Total}';
1731 $invoice_data{'total_amount'} =
1732 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1734 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1737 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1740 foreach my $credit ( $self->_items_credits ) {
1741 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1743 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1745 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1750 foreach my $payment ( $self->_items_payments ) {
1751 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1753 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1755 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1759 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1760 $invoice_data{'total_amount'} =
1761 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1763 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1766 push @filled_in, @total_fill;
1769 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1770 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1771 push @filled_in, $line;
1782 } elsif ( $format eq 'Text::Template' ) {
1784 my @detail_items = ();
1785 my @total_items = ();
1787 $invoice_data{'detail_items'} = \@detail_items;
1788 $invoice_data{'total_items'} = \@total_items;
1790 foreach my $line_item ( $self->_items ) {
1792 ext_description => [],
1794 $detail->{'ref'} = $line_item->{'pkgnum'};
1795 $detail->{'quantity'} = 1;
1796 $detail->{'description'} = _latex_escape($line_item->{'description'});
1797 if ( exists $line_item->{'ext_description'} ) {
1798 @{$detail->{'ext_description'}} = map {
1800 } @{$line_item->{'ext_description'}};
1802 $detail->{'amount'} = $line_item->{'amount'};
1803 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1805 push @detail_items, $detail;
1810 foreach my $tax ( $self->_items_tax ) {
1812 $total->{'total_item'} = _latex_escape($tax->{'description'});
1813 $taxtotal += $tax->{'amount'};
1814 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1815 push @total_items, $total;
1820 $total->{'total_item'} = 'Sub-total';
1821 $total->{'total_amount'} =
1822 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1823 unshift @total_items, $total;
1828 $total->{'total_item'} = '\textbf{Total}';
1829 $total->{'total_amount'} =
1830 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1831 push @total_items, $total;
1834 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1837 foreach my $credit ( $self->_items_credits ) {
1839 $total->{'total_item'} = _latex_escape($credit->{'description'});
1841 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1842 push @total_items, $total;
1846 foreach my $payment ( $self->_items_payments ) {
1848 $total->{'total_item'} = _latex_escape($payment->{'description'});
1850 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1851 push @total_items, $total;
1856 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1857 $total->{'total_amount'} =
1858 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1859 push @total_items, $total;
1863 die "guru meditation #54";
1866 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1867 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1871 ) or die "can't open temp file: $!\n";
1872 if ( $format eq 'old' ) {
1873 print $fh join('', @filled_in );
1874 } elsif ( $format eq 'Text::Template' ) {
1875 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1877 die "guru meditation #32";
1881 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1886 =item print_ps [ TIME [ , TEMPLATE ] ]
1888 Returns an postscript invoice, as a scalar.
1890 TIME an optional value used to control the printing of overdue messages. The
1891 default is now. It isn't the date of the invoice; that's the `_date' field.
1892 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1893 L<Time::Local> and L<Date::Parse> for conversion functions.
1900 my $file = $self->print_latex(@_);
1902 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1905 my $sfile = shell_quote $file;
1907 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1908 or die "pslatex $file.tex failed; see $file.log for details?\n";
1909 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1910 or die "pslatex $file.tex failed; see $file.log for details?\n";
1912 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1913 or die "dvips failed";
1915 open(POSTSCRIPT, "<$file.ps")
1916 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1918 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1921 while (<POSTSCRIPT>) {
1931 =item print_pdf [ TIME [ , TEMPLATE ] ]
1933 Returns an PDF invoice, as a scalar.
1935 TIME an optional value used to control the printing of overdue messages. The
1936 default is now. It isn't the date of the invoice; that's the `_date' field.
1937 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1938 L<Time::Local> and L<Date::Parse> for conversion functions.
1945 my $file = $self->print_latex(@_);
1947 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1950 #system('pdflatex', "$file.tex");
1951 #system('pdflatex', "$file.tex");
1952 #! LaTeX Error: Unknown graphics extension: .eps.
1954 my $sfile = shell_quote $file;
1956 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1957 or die "pslatex $file.tex failed; see $file.log for details?\n";
1958 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1959 or die "pslatex $file.tex failed; see $file.log for details?\n";
1961 #system('dvipdf', "$file.dvi", "$file.pdf" );
1963 "dvips -q -t letter -f $sfile.dvi ".
1964 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1967 or die "dvips | gs failed: $!";
1969 open(PDF, "<$file.pdf")
1970 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1972 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1985 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1987 Returns an HTML invoice, as a scalar.
1989 TIME an optional value used to control the printing of overdue messages. The
1990 default is now. It isn't the date of the invoice; that's the `_date' field.
1991 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1992 L<Time::Local> and L<Date::Parse> for conversion functions.
1994 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1995 when emailing the invoice as part of a multipart/related MIME email.
1999 #some falze laziness w/print_text and print_latex (and send_csv)
2001 my( $self, $today, $template, $cid ) = @_;
2004 my $cust_main = $self->cust_main;
2005 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2006 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2008 $template ||= $self->_agent_template;
2009 my $templatefile = 'invoice_html';
2010 my $suffix = length($template) ? "_$template" : '';
2011 $templatefile .= $suffix;
2012 my @html_template = map "$_\n", $conf->config($templatefile)
2013 or die "cannot load config file $templatefile";
2015 my $html_template = new Text::Template(
2017 SOURCE => \@html_template,
2018 DELIMITERS => [ '<%=', '%>' ],
2021 $html_template->compile()
2022 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2024 my %invoice_data = (
2025 'invnum' => $self->invnum,
2026 'date' => time2str('%b %o, %Y', $self->_date),
2027 'today' => time2str('%b %o, %Y', $today),
2028 'agent' => encode_entities($cust_main->agent->agent),
2029 'payname' => encode_entities($cust_main->payname),
2030 'company' => encode_entities($cust_main->company),
2031 'address1' => encode_entities($cust_main->address1),
2032 'address2' => encode_entities($cust_main->address2),
2033 'city' => encode_entities($cust_main->city),
2034 'state' => encode_entities($cust_main->state),
2035 'zip' => encode_entities($cust_main->zip),
2036 'terms' => $conf->config('invoice_default_terms')
2037 || 'Payable upon receipt',
2039 'template' => $template,
2040 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2044 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2045 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2047 $invoice_data{'returnaddress'} =
2048 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2050 $invoice_data{'returnaddress'} =
2053 s/\\\\\*?\s*$/<BR>/;
2054 s/\\hyphenation\{[\w\s\-]+\}//;
2057 $conf->config_orbase( 'invoice_latexreturnaddress',
2063 my $countrydefault = $conf->config('countrydefault') || 'US';
2064 if ( $cust_main->country eq $countrydefault ) {
2065 $invoice_data{'country'} = '';
2067 $invoice_data{'country'} =
2068 encode_entities(code2country($cust_main->country));
2072 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2073 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2075 $invoice_data{'notes'} =
2076 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2078 $invoice_data{'notes'} =
2080 s/%%(.*)$/<!-- $1 -->/;
2081 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2082 s/\\begin\{enumerate\}/<ol>/;
2084 s/\\end\{enumerate\}/<\/ol>/;
2085 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2088 $conf->config_orbase('invoice_latexnotes', $template)
2092 # #do variable substitutions in notes
2093 # $invoice_data{'notes'} =
2095 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2096 # $conf->config_orbase('invoice_latexnotes', $suffix)
2100 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2101 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2103 $invoice_data{'footer'} =
2104 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2106 $invoice_data{'footer'} =
2107 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2108 $conf->config_orbase('invoice_latexfooter', $template)
2112 $invoice_data{'po_line'} =
2113 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2114 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2117 my $money_char = $conf->config('money_char') || '$';
2119 foreach my $line_item ( $self->_items ) {
2121 ext_description => [],
2123 $detail->{'ref'} = $line_item->{'pkgnum'};
2124 $detail->{'description'} = encode_entities($line_item->{'description'});
2125 if ( exists $line_item->{'ext_description'} ) {
2126 @{$detail->{'ext_description'}} = map {
2127 encode_entities($_);
2128 } @{$line_item->{'ext_description'}};
2130 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2131 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2133 push @{$invoice_data{'detail_items'}}, $detail;
2138 foreach my $tax ( $self->_items_tax ) {
2140 $total->{'total_item'} = encode_entities($tax->{'description'});
2141 $taxtotal += $tax->{'amount'};
2142 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2143 push @{$invoice_data{'total_items'}}, $total;
2148 $total->{'total_item'} = 'Sub-total';
2149 $total->{'total_amount'} =
2150 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2151 unshift @{$invoice_data{'total_items'}}, $total;
2154 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2157 $total->{'total_item'} = '<b>Total</b>';
2158 $total->{'total_amount'} =
2159 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2160 push @{$invoice_data{'total_items'}}, $total;
2163 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2166 foreach my $credit ( $self->_items_credits ) {
2168 $total->{'total_item'} = encode_entities($credit->{'description'});
2170 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2171 push @{$invoice_data{'total_items'}}, $total;
2175 foreach my $payment ( $self->_items_payments ) {
2177 $total->{'total_item'} = encode_entities($payment->{'description'});
2179 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2180 push @{$invoice_data{'total_items'}}, $total;
2185 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2186 $total->{'total_amount'} =
2187 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2188 push @{$invoice_data{'total_items'}}, $total;
2191 $html_template->fill_in( HASH => \%invoice_data);
2194 # quick subroutine for print_latex
2196 # There are ten characters that LaTeX treats as special characters, which
2197 # means that they do not simply typeset themselves:
2198 # # $ % & ~ _ ^ \ { }
2200 # TeX ignores blanks following an escaped character; if you want a blank (as
2201 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2205 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2206 $value =~ s/([<>])/\$$1\$/g;
2210 #utility methods for print_*
2212 sub balance_due_msg {
2214 my $msg = 'Balance Due';
2215 return $msg unless $conf->exists('invoice_default_terms');
2216 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2217 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2218 } elsif ( $conf->config('invoice_default_terms') ) {
2219 $msg .= ' - '. $conf->config('invoice_default_terms');
2226 my @display = scalar(@_)
2228 : qw( _items_previous _items_pkg );
2229 #: qw( _items_pkg );
2230 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2232 foreach my $display ( @display ) {
2233 push @b, $self->$display(@_);
2238 sub _items_previous {
2240 my $cust_main = $self->cust_main;
2241 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2243 foreach ( @pr_cust_bill ) {
2245 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2246 ' ('. time2str('%x',$_->_date). ')',
2247 #'pkgpart' => 'N/A',
2249 'amount' => sprintf("%.2f", $_->owed),
2255 # 'description' => 'Previous Balance',
2256 # #'pkgpart' => 'N/A',
2257 # 'pkgnum' => 'N/A',
2258 # 'amount' => sprintf("%10.2f", $pr_total ),
2259 # 'ext_description' => [ map {
2260 # "Invoice ". $_->invnum.
2261 # " (". time2str("%x",$_->_date). ") ".
2262 # sprintf("%10.2f", $_->owed)
2263 # } @pr_cust_bill ],
2270 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2271 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2276 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2277 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2280 sub _items_cust_bill_pkg {
2282 my $cust_bill_pkg = shift;
2285 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2287 my $desc = $cust_bill_pkg->desc;
2289 if ( $cust_bill_pkg->pkgnum > 0 ) {
2291 if ( $cust_bill_pkg->setup != 0 ) {
2292 my $description = $desc;
2293 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2294 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2295 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2297 description => $description,
2298 #pkgpart => $part_pkg->pkgpart,
2299 pkgnum => $cust_bill_pkg->pkgnum,
2300 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2301 ext_description => \@d,
2305 if ( $cust_bill_pkg->recur != 0 ) {
2307 description => "$desc (" .
2308 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2309 time2str('%x', $cust_bill_pkg->edate). ')',
2310 #pkgpart => $part_pkg->pkgpart,
2311 pkgnum => $cust_bill_pkg->pkgnum,
2312 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2314 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2315 $cust_bill_pkg->sdate),
2316 $cust_bill_pkg->details,
2321 } else { #pkgnum tax or one-shot line item (??)
2323 if ( $cust_bill_pkg->setup != 0 ) {
2325 'description' => $desc,
2326 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2329 if ( $cust_bill_pkg->recur != 0 ) {
2331 'description' => "$desc (".
2332 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2333 time2str("%x", $cust_bill_pkg->edate). ')',
2334 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2346 sub _items_credits {
2351 foreach ( $self->cust_credited ) {
2353 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2355 my $reason = $_->cust_credit->reason;
2356 #my $reason = substr($_->cust_credit->reason,0,32);
2357 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2358 $reason = " ($reason) " if $reason;
2360 #'description' => 'Credit ref\#'. $_->crednum.
2361 # " (". time2str("%x",$_->cust_credit->_date) .")".
2363 'description' => 'Credit applied '.
2364 time2str("%x",$_->cust_credit->_date). $reason,
2365 'amount' => sprintf("%.2f",$_->amount),
2368 #foreach ( @cr_cust_credit ) {
2370 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2371 # $money_char. sprintf("%10.2f",$_->credited)
2379 sub _items_payments {
2383 #get & print payments
2384 foreach ( $self->cust_bill_pay ) {
2386 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2389 'description' => "Payment received ".
2390 time2str("%x",$_->cust_pay->_date ),
2391 'amount' => sprintf("%.2f", $_->amount )
2409 sub process_reprint {
2410 process_re_X('print', @_);
2417 sub process_reemail {
2418 process_re_X('email', @_);
2426 process_re_X('fax', @_);
2429 use Storable qw(thaw);
2433 my( $method, $job ) = ( shift, shift );
2435 my $param = thaw(decode_base64(shift));
2436 warn Dumper($param) if $DEBUG;
2447 my($method, $job, %param ) = @_;
2448 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2450 #some false laziness w/search/cust_bill.html
2452 my $orderby = 'ORDER BY cust_bill._date';
2456 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2457 push @where, "cust_bill._date >= $1";
2459 if ( $param{'end'} =~ /^(\d+)$/ ) {
2460 push @where, "cust_bill._date < $1";
2462 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2463 push @where, "cust_main.agentnum = $1";
2467 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2468 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2469 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2470 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2472 push @where, "0 != $owed"
2475 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2478 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2480 my $addl_from = 'left join cust_main using ( custnum )';
2482 if ( $param{'newest_percust'} ) {
2483 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2484 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2485 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2488 my @cust_bill = qsearch( 'cust_bill',
2490 "$distinct cust_bill.*",
2496 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2497 foreach my $cust_bill ( @cust_bill ) {
2498 $cust_bill->$method();
2500 if ( $job ) { #progressbar foo
2502 if ( time - $min_sec > $last ) {
2503 my $error = $job->update_statustext(
2504 int( 100 * $num / scalar(@cust_bill) )
2506 die $error if $error;
2521 print_text formatting (and some logic :/) is in source, but needs to be
2522 slurped in from a file. Also number of lines ($=).
2526 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2527 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base