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"
743 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
744 and YYMMDDHHMMSS is a timestamp.
746 See L</print_csv> for a description of the output format.
751 my($self, %opt) = @_;
755 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
756 mkdir $spooldir, 0700 unless -d $spooldir;
758 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
759 my $file = "$spooldir/$tracctnum.csv";
761 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
763 open(CSV, ">$file") or die "can't open $file: $!";
771 if ( $opt{protocol} eq 'ftp' ) {
772 eval "use Net::FTP;";
774 $net = Net::FTP->new($opt{server}) or die @$;
776 die "unknown protocol: $opt{protocol}";
779 $net->login( $opt{username}, $opt{password} )
780 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
782 $net->binary or die "can't set binary mode";
784 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
786 $net->put($file) or die "can't put $file: $!";
796 Spools CSV invoice data.
802 =item format - 'default' or 'billco'
804 =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>).
806 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
813 my($self, %opt) = @_;
815 my $cust_main = $self->cust_main;
817 if ( $opt{'dest'} ) {
818 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
819 $cust_main->invoicing_list;
820 return 'N/A' unless $invoicing_list{$opt{'dest'}}
821 || ! keys %invoicing_list;
824 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
825 mkdir $spooldir, 0700 unless -d $spooldir;
827 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
831 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
832 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
835 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
837 open(CSV, ">>$file") or die "can't open $file: $!";
843 if ( lc($opt{'format'}) eq 'billco' ) {
850 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
853 open(CSV,">>$file") or die "can't open $file: $!";
867 =item print_csv OPTION => VALUE, ...
869 Returns CSV data for this invoice.
873 format - 'default' or 'billco'
875 Returns a list consisting of two scalars. The first is a single line of CSV
876 header information for this invoice. The second is one or more lines of CSV
877 detail information for this invoice.
879 If I<format> is not specified or "default", the fields of the CSV file are as
882 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
886 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
888 B<record_type> is C<cust_bill> for the initial header line only. The
889 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
890 fields are filled in.
892 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
893 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
896 =item invnum - invoice number
898 =item custnum - customer number
900 =item _date - invoice date
902 =item charged - total invoice amount
904 =item first - customer first name
906 =item last - customer first name
908 =item company - company name
910 =item address1 - address line 1
912 =item address2 - address line 1
922 =item pkg - line item description
924 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
926 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
928 =item sdate - start date for recurring fee
930 =item edate - end date for recurring fee
934 If I<format> is "billco", the fields of the header CSV file are as follows:
936 +-------------------------------------------------------------------+
937 | FORMAT HEADER FILE |
938 |-------------------------------------------------------------------|
939 | Field | Description | Name | Type | Width |
940 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
941 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
942 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
943 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
944 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
945 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
946 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
947 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
948 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
949 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
950 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
951 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
952 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
953 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
954 | 15 | Previous Balance | BALFWD | NUM* | 9 |
955 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
956 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
957 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
958 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
959 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
960 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
961 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
962 | 23 | Y/N | AGESWITCH | CHAR | 1 |
963 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
964 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
965 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
966 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
967 | 28 | State Tax*** | STATETAX | NUM* | 9 |
968 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
969 +-------+-------------------------------+------------+------+-------+
971 If I<format> is "billco", the fields of the detail CSV file are as follows:
973 FORMAT FOR DETAIL FILE
975 Field | Description | Name | Type | Width
976 1 | N/A-Leave Empty | RC | CHAR | 2
977 2 | N/A-Leave Empty | CUSTID | CHAR | 15
978 3 | Account Number | TRACCTNUM | CHAR | 15
979 4 | Invoice Number | TRINVOICE | CHAR | 15
980 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
981 6 | Transaction Detail | DETAILS | CHAR | 100
982 7 | Amount | AMT | NUM* | 9
983 8 | Line Format Control** | LNCTRL | CHAR | 2
984 9 | Grouping Code | GROUP | CHAR | 2
985 10 | User Defined | ACCT CODE | CHAR | 15
990 my($self, %opt) = @_;
992 eval "use Text::CSV_XS";
995 my $cust_main = $self->cust_main;
997 my $csv = Text::CSV_XS->new({'always_quote'=>1});
999 if ( lc($opt{'format'}) eq 'billco' ) {
1002 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1005 if ( $conf->exists('invoice_default_terms')
1006 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1007 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1010 my( $previous_balance, @unused ) = $self->previous; #previous balance
1012 my $pmt_cr_applied = 0;
1013 $pmt_cr_applied += $_->{'amount'}
1014 foreach ( $self->_items_payments, $self->_items_credits ) ;
1016 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1019 '', # 1 | N/A-Leave Empty CHAR 2
1020 '', # 2 | N/A-Leave Empty CHAR 15
1021 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1022 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1023 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1024 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1025 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1026 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1027 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1028 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1029 '', # 10 | Ancillary Billing Information CHAR 30
1030 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1031 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1034 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1037 $duedate, # 14 | Bill Due Date CHAR 10
1039 $previous_balance, # 15 | Previous Balance NUM* 9
1040 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1041 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1042 $totaldue, # 18 | Total Amt Due NUM* 9
1043 $totaldue, # 19 | Total Amt Due NUM* 9
1044 '', # 20 | 30 Day Aging NUM* 9
1045 '', # 21 | 60 Day Aging NUM* 9
1046 '', # 22 | 90 Day Aging NUM* 9
1047 'N', # 23 | Y/N CHAR 1
1048 '', # 24 | Remittance automation CHAR 100
1049 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1050 $self->custnum, # 26 | Customer Reference Number CHAR 15
1051 '0', # 27 | Federal Tax*** NUM* 9
1052 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1053 '0', # 29 | Other Taxes & Fees*** NUM* 9
1062 time2str("%x", $self->_date),
1063 sprintf("%.2f", $self->charged),
1064 ( map { $cust_main->getfield($_) }
1065 qw( first last company address1 address2 city state zip country ) ),
1067 ) or die "can't create csv";
1070 my $header = $csv->string. "\n";
1073 if ( lc($opt{'format'}) eq 'billco' ) {
1076 foreach my $item ( $self->_items_pkg ) {
1079 '', # 1 | N/A-Leave Empty CHAR 2
1080 '', # 2 | N/A-Leave Empty CHAR 15
1081 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1082 $self->invnum, # 4 | Invoice Number CHAR 15
1083 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1084 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1085 $item->{'amount'}, # 7 | Amount NUM* 9
1086 '', # 8 | Line Format Control** CHAR 2
1087 '', # 9 | Grouping Code CHAR 2
1088 '', # 10 | User Defined CHAR 15
1091 $detail .= $csv->string. "\n";
1097 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1099 my($pkg, $setup, $recur, $sdate, $edate);
1100 if ( $cust_bill_pkg->pkgnum ) {
1102 ($pkg, $setup, $recur, $sdate, $edate) = (
1103 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1104 ( $cust_bill_pkg->setup != 0
1105 ? sprintf("%.2f", $cust_bill_pkg->setup )
1107 ( $cust_bill_pkg->recur != 0
1108 ? sprintf("%.2f", $cust_bill_pkg->recur )
1110 ( $cust_bill_pkg->sdate
1111 ? time2str("%x", $cust_bill_pkg->sdate)
1113 ($cust_bill_pkg->edate
1114 ?time2str("%x", $cust_bill_pkg->edate)
1118 } else { #pkgnum tax
1119 next unless $cust_bill_pkg->setup != 0;
1120 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1121 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1123 ($pkg, $setup, $recur, $sdate, $edate) =
1124 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1130 ( map { '' } (1..11) ),
1131 ($pkg, $setup, $recur, $sdate, $edate)
1132 ) or die "can't create csv";
1134 $detail .= $csv->string. "\n";
1140 ( $header, $detail );
1146 Pays this invoice with a compliemntary payment. If there is an error,
1147 returns the error, otherwise returns false.
1153 my $cust_pay = new FS::cust_pay ( {
1154 'invnum' => $self->invnum,
1155 'paid' => $self->owed,
1158 'payinfo' => $self->cust_main->payinfo,
1166 Attempts to pay this invoice with a credit card payment via a
1167 Business::OnlinePayment realtime gateway. See
1168 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1169 for supported processors.
1175 $self->realtime_bop( 'CC', @_ );
1180 Attempts to pay this invoice with an electronic check (ACH) payment via a
1181 Business::OnlinePayment realtime gateway. See
1182 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1183 for supported processors.
1189 $self->realtime_bop( 'ECHECK', @_ );
1194 Attempts to pay this invoice with phone bill (LEC) payment via a
1195 Business::OnlinePayment realtime gateway. See
1196 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1197 for supported processors.
1203 $self->realtime_bop( 'LEC', @_ );
1207 my( $self, $method ) = @_;
1209 my $cust_main = $self->cust_main;
1210 my $balance = $cust_main->balance;
1211 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1212 $amount = sprintf("%.2f", $amount);
1213 return "not run (balance $balance)" unless $amount > 0;
1215 my $description = 'Internet Services';
1216 if ( $conf->exists('business-onlinepayment-description') ) {
1217 my $dtempl = $conf->config('business-onlinepayment-description');
1219 my $agent_obj = $cust_main->agent
1220 or die "can't retreive agent for $cust_main (agentnum ".
1221 $cust_main->agentnum. ")";
1222 my $agent = $agent_obj->agent;
1223 my $pkgs = join(', ',
1224 map { $_->cust_pkg->part_pkg->pkg }
1225 grep { $_->pkgnum } $self->cust_bill_pkg
1227 $description = eval qq("$dtempl");
1230 $cust_main->realtime_bop($method, $amount,
1231 'description' => $description,
1232 'invnum' => $self->invnum,
1239 Adds a payment for this invoice to the pending credit card batch (see
1240 L<FS::cust_pay_batch>).
1246 my $cust_main = $self->cust_main;
1248 my $cust_pay_batch = new FS::cust_pay_batch ( {
1249 'invnum' => $self->getfield('invnum'),
1250 'custnum' => $cust_main->getfield('custnum'),
1251 'last' => $cust_main->getfield('last'),
1252 'first' => $cust_main->getfield('first'),
1253 'address1' => $cust_main->getfield('address1'),
1254 'address2' => $cust_main->getfield('address2'),
1255 'city' => $cust_main->getfield('city'),
1256 'state' => $cust_main->getfield('state'),
1257 'zip' => $cust_main->getfield('zip'),
1258 'country' => $cust_main->getfield('country'),
1259 'cardnum' => $cust_main->payinfo,
1260 'exp' => $cust_main->getfield('paydate'),
1261 'payname' => $cust_main->getfield('payname'),
1262 'amount' => $self->owed,
1264 my $error = $cust_pay_batch->insert;
1265 die $error if $error;
1270 sub _agent_template {
1272 $self->_agent_plandata('agent_templatename');
1275 sub _agent_invoice_from {
1277 $self->_agent_plandata('agent_invoice_from');
1280 sub _agent_plandata {
1281 my( $self, $option ) = @_;
1283 my $part_bill_event = qsearchs( 'part_bill_event',
1285 'payby' => $self->cust_main->payby,
1286 'plan' => 'send_agent',
1287 'plandata' => { 'op' => '~',
1288 'value' => "(^|\n)agentnum ".
1290 $self->cust_main->agentnum.
1296 'ORDER BY seconds LIMIT 1'
1299 return '' unless $part_bill_event;
1301 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1304 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1305 " plandata for $option";
1311 =item print_text [ TIME [ , TEMPLATE ] ]
1313 Returns an text invoice, as a list of lines.
1315 TIME an optional value used to control the printing of overdue messages. The
1316 default is now. It isn't the date of the invoice; that's the `_date' field.
1317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1318 L<Time::Local> and L<Date::Parse> for conversion functions.
1322 #still some false laziness w/_items stuff (and send_csv)
1325 my( $self, $today, $template ) = @_;
1328 # my $invnum = $self->invnum;
1329 my $cust_main = $self->cust_main;
1330 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1331 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1333 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1334 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1335 #my $balance_due = $self->owed + $pr_total - $cr_total;
1336 my $balance_due = $self->owed + $pr_total;
1339 #my($description,$amount);
1343 foreach ( @pr_cust_bill ) {
1345 "Previous Balance, Invoice #". $_->invnum.
1346 " (". time2str("%x",$_->_date). ")",
1347 $money_char. sprintf("%10.2f",$_->owed)
1350 if (@pr_cust_bill) {
1351 push @buf,['','-----------'];
1352 push @buf,[ 'Total Previous Balance',
1353 $money_char. sprintf("%10.2f",$pr_total ) ];
1358 foreach my $cust_bill_pkg (
1359 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1360 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1363 my $desc = $cust_bill_pkg->desc;
1365 if ( $cust_bill_pkg->pkgnum > 0 ) {
1367 if ( $cust_bill_pkg->setup != 0 ) {
1368 my $description = $desc;
1369 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1370 push @buf, [ $description,
1371 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1373 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1374 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1377 if ( $cust_bill_pkg->recur != 0 ) {
1379 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1380 time2str("%x", $cust_bill_pkg->edate) . ")",
1381 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1384 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1385 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1386 $cust_bill_pkg->sdate );
1389 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1391 } else { #pkgnum tax or one-shot line item
1393 if ( $cust_bill_pkg->setup != 0 ) {
1395 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1397 if ( $cust_bill_pkg->recur != 0 ) {
1398 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1399 . time2str("%x", $cust_bill_pkg->edate). ")",
1400 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1408 push @buf,['','-----------'];
1409 push @buf,['Total New Charges',
1410 $money_char. sprintf("%10.2f",$self->charged) ];
1413 push @buf,['','-----------'];
1414 push @buf,['Total Charges',
1415 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1419 foreach ( $self->cust_credited ) {
1421 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1423 my $reason = substr($_->cust_credit->reason,0,32);
1424 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1425 $reason = " ($reason) " if $reason;
1427 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1429 $money_char. sprintf("%10.2f",$_->amount)
1432 #foreach ( @cr_cust_credit ) {
1434 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1435 # $money_char. sprintf("%10.2f",$_->credited)
1439 #get & print payments
1440 foreach ( $self->cust_bill_pay ) {
1442 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1445 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1446 $money_char. sprintf("%10.2f",$_->amount )
1451 my $balance_due_msg = $self->balance_due_msg;
1453 push @buf,['','-----------'];
1454 push @buf,[$balance_due_msg, $money_char.
1455 sprintf("%10.2f", $balance_due ) ];
1457 #create the template
1458 $template ||= $self->_agent_template;
1459 my $templatefile = 'invoice_template';
1460 $templatefile .= "_$template" if length($template);
1461 my @invoice_template = $conf->config($templatefile)
1462 or die "cannot load config file $templatefile";
1465 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1466 /invoice_lines\((\d*)\)/;
1467 $invoice_lines += $1 || scalar(@buf);
1470 die "no invoice_lines() functions in template?" unless $wasfunc;
1471 my $invoice_template = new Text::Template (
1473 SOURCE => [ map "$_\n", @invoice_template ],
1474 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1475 $invoice_template->compile()
1476 or die "can't compile template: $Text::Template::ERROR";
1478 #setup template variables
1479 package FS::cust_bill::_template; #!
1480 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1482 $invnum = $self->invnum;
1483 $date = $self->_date;
1485 $agent = $self->cust_main->agent->agent;
1487 if ( $FS::cust_bill::invoice_lines ) {
1489 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1491 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1496 #format address (variable for the template)
1498 @address = ( '', '', '', '', '', '' );
1499 package FS::cust_bill; #!
1500 $FS::cust_bill::_template::address[$l++] =
1501 $cust_main->payname.
1502 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1503 ? " (P.O. #". $cust_main->payinfo. ")"
1507 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1508 if $cust_main->company;
1509 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1510 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1511 if $cust_main->address2;
1512 $FS::cust_bill::_template::address[$l++] =
1513 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1515 my $countrydefault = $conf->config('countrydefault') || 'US';
1516 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1517 unless $cust_main->country eq $countrydefault;
1519 # #overdue? (variable for the template)
1520 # $FS::cust_bill::_template::overdue = (
1522 # && $today > $self->_date
1523 ## && $self->printed > 1
1524 # && $self->printed > 0
1527 #and subroutine for the template
1528 sub FS::cust_bill::_template::invoice_lines {
1529 my $lines = shift || scalar(@buf);
1531 scalar(@buf) ? shift @buf : [ '', '' ];
1537 $FS::cust_bill::_template::page = 1;
1541 push @collect, split("\n",
1542 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1544 $FS::cust_bill::_template::page++;
1547 map "$_\n", @collect;
1551 =item print_latex [ TIME [ , TEMPLATE ] ]
1553 Internal method - returns a filename of a filled-in LaTeX template for this
1554 invoice (Note: add ".tex" to get the actual filename).
1556 See print_ps and print_pdf for methods that return PostScript and PDF output.
1558 TIME an optional value used to control the printing of overdue messages. The
1559 default is now. It isn't the date of the invoice; that's the `_date' field.
1560 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1561 L<Time::Local> and L<Date::Parse> for conversion functions.
1565 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1568 my( $self, $today, $template ) = @_;
1570 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1573 my $cust_main = $self->cust_main;
1574 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1575 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1577 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1578 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1579 #my $balance_due = $self->owed + $pr_total - $cr_total;
1580 my $balance_due = $self->owed + $pr_total;
1582 #create the template
1583 $template ||= $self->_agent_template;
1584 my $templatefile = 'invoice_latex';
1585 my $suffix = length($template) ? "_$template" : '';
1586 $templatefile .= $suffix;
1587 my @invoice_template = map "$_\n", $conf->config($templatefile)
1588 or die "cannot load config file $templatefile";
1590 my($format, $text_template);
1591 if ( grep { /^%%Detail/ } @invoice_template ) {
1592 #change this to a die when the old code is removed
1593 warn "old-style invoice template $templatefile; ".
1594 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1597 $format = 'Text::Template';
1598 $text_template = new Text::Template(
1600 SOURCE => \@invoice_template,
1601 DELIMITERS => [ '[@--', '--@]' ],
1604 $text_template->compile()
1605 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1609 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1610 $returnaddress = join("\n",
1611 $conf->config_orbase('invoice_latexreturnaddress', $template)
1614 $returnaddress = '~';
1617 my %invoice_data = (
1618 'invnum' => $self->invnum,
1619 'date' => time2str('%b %o, %Y', $self->_date),
1620 'today' => time2str('%b %o, %Y', $today),
1621 'agent' => _latex_escape($cust_main->agent->agent),
1622 'payname' => _latex_escape($cust_main->payname),
1623 'company' => _latex_escape($cust_main->company),
1624 'address1' => _latex_escape($cust_main->address1),
1625 'address2' => _latex_escape($cust_main->address2),
1626 'city' => _latex_escape($cust_main->city),
1627 'state' => _latex_escape($cust_main->state),
1628 'zip' => _latex_escape($cust_main->zip),
1629 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1630 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1631 'returnaddress' => $returnaddress,
1633 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1634 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1635 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1638 my $countrydefault = $conf->config('countrydefault') || 'US';
1639 if ( $cust_main->country eq $countrydefault ) {
1640 $invoice_data{'country'} = '';
1642 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1645 $invoice_data{'notes'} =
1647 # #do variable substitutions in notes
1648 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1649 $conf->config_orbase('invoice_latexnotes', $template)
1651 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1654 $invoice_data{'footer'} =~ s/\n+$//;
1655 $invoice_data{'smallfooter'} =~ s/\n+$//;
1656 $invoice_data{'notes'} =~ s/\n+$//;
1658 $invoice_data{'po_line'} =
1659 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1660 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1664 if ( $format eq 'old' ) {
1667 my @total_item = ();
1668 while ( @invoice_template ) {
1669 my $line = shift @invoice_template;
1671 if ( $line =~ /^%%Detail\s*$/ ) {
1673 while ( ( my $line_item_line = shift @invoice_template )
1674 !~ /^%%EndDetail\s*$/ ) {
1675 push @line_item, $line_item_line;
1677 foreach my $line_item ( $self->_items ) {
1678 #foreach my $line_item ( $self->_items_pkg ) {
1679 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1680 $invoice_data{'description'} =
1681 _latex_escape($line_item->{'description'});
1682 if ( exists $line_item->{'ext_description'} ) {
1683 $invoice_data{'description'} .=
1684 "\\tabularnewline\n~~".
1685 join( "\\tabularnewline\n~~",
1686 map _latex_escape($_), @{$line_item->{'ext_description'}}
1689 $invoice_data{'amount'} = $line_item->{'amount'};
1690 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1692 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1695 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1697 while ( ( my $total_item_line = shift @invoice_template )
1698 !~ /^%%EndTotalDetails\s*$/ ) {
1699 push @total_item, $total_item_line;
1702 my @total_fill = ();
1705 foreach my $tax ( $self->_items_tax ) {
1706 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1707 $taxtotal += $tax->{'amount'};
1708 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1710 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1715 $invoice_data{'total_item'} = 'Sub-total';
1716 $invoice_data{'total_amount'} =
1717 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1718 unshift @total_fill,
1719 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1723 $invoice_data{'total_item'} = '\textbf{Total}';
1724 $invoice_data{'total_amount'} =
1725 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1727 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1730 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1733 foreach my $credit ( $self->_items_credits ) {
1734 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1736 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1738 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1743 foreach my $payment ( $self->_items_payments ) {
1744 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1746 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1748 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1752 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1753 $invoice_data{'total_amount'} =
1754 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1756 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1759 push @filled_in, @total_fill;
1762 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1763 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1764 push @filled_in, $line;
1775 } elsif ( $format eq 'Text::Template' ) {
1777 my @detail_items = ();
1778 my @total_items = ();
1780 $invoice_data{'detail_items'} = \@detail_items;
1781 $invoice_data{'total_items'} = \@total_items;
1783 foreach my $line_item ( $self->_items ) {
1785 ext_description => [],
1787 $detail->{'ref'} = $line_item->{'pkgnum'};
1788 $detail->{'quantity'} = 1;
1789 $detail->{'description'} = _latex_escape($line_item->{'description'});
1790 if ( exists $line_item->{'ext_description'} ) {
1791 @{$detail->{'ext_description'}} = map {
1793 } @{$line_item->{'ext_description'}};
1795 $detail->{'amount'} = $line_item->{'amount'};
1796 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1798 push @detail_items, $detail;
1803 foreach my $tax ( $self->_items_tax ) {
1805 $total->{'total_item'} = _latex_escape($tax->{'description'});
1806 $taxtotal += $tax->{'amount'};
1807 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1808 push @total_items, $total;
1813 $total->{'total_item'} = 'Sub-total';
1814 $total->{'total_amount'} =
1815 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1816 unshift @total_items, $total;
1821 $total->{'total_item'} = '\textbf{Total}';
1822 $total->{'total_amount'} =
1823 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1824 push @total_items, $total;
1827 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1830 foreach my $credit ( $self->_items_credits ) {
1832 $total->{'total_item'} = _latex_escape($credit->{'description'});
1834 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1835 push @total_items, $total;
1839 foreach my $payment ( $self->_items_payments ) {
1841 $total->{'total_item'} = _latex_escape($payment->{'description'});
1843 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1844 push @total_items, $total;
1849 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1850 $total->{'total_amount'} =
1851 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1852 push @total_items, $total;
1856 die "guru meditation #54";
1859 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1860 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1864 ) or die "can't open temp file: $!\n";
1865 if ( $format eq 'old' ) {
1866 print $fh join('', @filled_in );
1867 } elsif ( $format eq 'Text::Template' ) {
1868 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1870 die "guru meditation #32";
1874 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1879 =item print_ps [ TIME [ , TEMPLATE ] ]
1881 Returns an postscript invoice, as a scalar.
1883 TIME an optional value used to control the printing of overdue messages. The
1884 default is now. It isn't the date of the invoice; that's the `_date' field.
1885 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1886 L<Time::Local> and L<Date::Parse> for conversion functions.
1893 my $file = $self->print_latex(@_);
1895 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1898 my $sfile = shell_quote $file;
1900 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1901 or die "pslatex $file.tex failed; see $file.log for details?\n";
1902 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1903 or die "pslatex $file.tex failed; see $file.log for details?\n";
1905 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1906 or die "dvips failed";
1908 open(POSTSCRIPT, "<$file.ps")
1909 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1911 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1914 while (<POSTSCRIPT>) {
1924 =item print_pdf [ TIME [ , TEMPLATE ] ]
1926 Returns an PDF invoice, as a scalar.
1928 TIME an optional value used to control the printing of overdue messages. The
1929 default is now. It isn't the date of the invoice; that's the `_date' field.
1930 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1931 L<Time::Local> and L<Date::Parse> for conversion functions.
1938 my $file = $self->print_latex(@_);
1940 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1943 #system('pdflatex', "$file.tex");
1944 #system('pdflatex', "$file.tex");
1945 #! LaTeX Error: Unknown graphics extension: .eps.
1947 my $sfile = shell_quote $file;
1949 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1950 or die "pslatex $file.tex failed; see $file.log for details?\n";
1951 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1952 or die "pslatex $file.tex failed; see $file.log for details?\n";
1954 #system('dvipdf', "$file.dvi", "$file.pdf" );
1956 "dvips -q -t letter -f $sfile.dvi ".
1957 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1960 or die "dvips | gs failed: $!";
1962 open(PDF, "<$file.pdf")
1963 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1965 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1978 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1980 Returns an HTML invoice, as a scalar.
1982 TIME an optional value used to control the printing of overdue messages. The
1983 default is now. It isn't the date of the invoice; that's the `_date' field.
1984 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1985 L<Time::Local> and L<Date::Parse> for conversion functions.
1987 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1988 when emailing the invoice as part of a multipart/related MIME email.
1992 #some falze laziness w/print_text and print_latex (and send_csv)
1994 my( $self, $today, $template, $cid ) = @_;
1997 my $cust_main = $self->cust_main;
1998 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1999 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2001 $template ||= $self->_agent_template;
2002 my $templatefile = 'invoice_html';
2003 my $suffix = length($template) ? "_$template" : '';
2004 $templatefile .= $suffix;
2005 my @html_template = map "$_\n", $conf->config($templatefile)
2006 or die "cannot load config file $templatefile";
2008 my $html_template = new Text::Template(
2010 SOURCE => \@html_template,
2011 DELIMITERS => [ '<%=', '%>' ],
2014 $html_template->compile()
2015 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2017 my %invoice_data = (
2018 'invnum' => $self->invnum,
2019 'date' => time2str('%b %o, %Y', $self->_date),
2020 'today' => time2str('%b %o, %Y', $today),
2021 'agent' => encode_entities($cust_main->agent->agent),
2022 'payname' => encode_entities($cust_main->payname),
2023 'company' => encode_entities($cust_main->company),
2024 'address1' => encode_entities($cust_main->address1),
2025 'address2' => encode_entities($cust_main->address2),
2026 'city' => encode_entities($cust_main->city),
2027 'state' => encode_entities($cust_main->state),
2028 'zip' => encode_entities($cust_main->zip),
2029 'terms' => $conf->config('invoice_default_terms')
2030 || 'Payable upon receipt',
2032 'template' => $template,
2033 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2037 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2038 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2040 $invoice_data{'returnaddress'} =
2041 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2043 $invoice_data{'returnaddress'} =
2046 s/\\\\\*?\s*$/<BR>/;
2047 s/\\hyphenation\{[\w\s\-]+\}//;
2050 $conf->config_orbase( 'invoice_latexreturnaddress',
2056 my $countrydefault = $conf->config('countrydefault') || 'US';
2057 if ( $cust_main->country eq $countrydefault ) {
2058 $invoice_data{'country'} = '';
2060 $invoice_data{'country'} =
2061 encode_entities(code2country($cust_main->country));
2065 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2066 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2068 $invoice_data{'notes'} =
2069 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2071 $invoice_data{'notes'} =
2073 s/%%(.*)$/<!-- $1 -->/;
2074 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2075 s/\\begin\{enumerate\}/<ol>/;
2077 s/\\end\{enumerate\}/<\/ol>/;
2078 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2081 $conf->config_orbase('invoice_latexnotes', $template)
2085 # #do variable substitutions in notes
2086 # $invoice_data{'notes'} =
2088 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2089 # $conf->config_orbase('invoice_latexnotes', $suffix)
2093 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2094 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2096 $invoice_data{'footer'} =
2097 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2099 $invoice_data{'footer'} =
2100 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2101 $conf->config_orbase('invoice_latexfooter', $template)
2105 $invoice_data{'po_line'} =
2106 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2107 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2110 my $money_char = $conf->config('money_char') || '$';
2112 foreach my $line_item ( $self->_items ) {
2114 ext_description => [],
2116 $detail->{'ref'} = $line_item->{'pkgnum'};
2117 $detail->{'description'} = encode_entities($line_item->{'description'});
2118 if ( exists $line_item->{'ext_description'} ) {
2119 @{$detail->{'ext_description'}} = map {
2120 encode_entities($_);
2121 } @{$line_item->{'ext_description'}};
2123 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2124 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2126 push @{$invoice_data{'detail_items'}}, $detail;
2131 foreach my $tax ( $self->_items_tax ) {
2133 $total->{'total_item'} = encode_entities($tax->{'description'});
2134 $taxtotal += $tax->{'amount'};
2135 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2136 push @{$invoice_data{'total_items'}}, $total;
2141 $total->{'total_item'} = 'Sub-total';
2142 $total->{'total_amount'} =
2143 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2144 unshift @{$invoice_data{'total_items'}}, $total;
2147 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2150 $total->{'total_item'} = '<b>Total</b>';
2151 $total->{'total_amount'} =
2152 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2153 push @{$invoice_data{'total_items'}}, $total;
2156 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2159 foreach my $credit ( $self->_items_credits ) {
2161 $total->{'total_item'} = encode_entities($credit->{'description'});
2163 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2164 push @{$invoice_data{'total_items'}}, $total;
2168 foreach my $payment ( $self->_items_payments ) {
2170 $total->{'total_item'} = encode_entities($payment->{'description'});
2172 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2173 push @{$invoice_data{'total_items'}}, $total;
2178 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2179 $total->{'total_amount'} =
2180 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2181 push @{$invoice_data{'total_items'}}, $total;
2184 $html_template->fill_in( HASH => \%invoice_data);
2187 # quick subroutine for print_latex
2189 # There are ten characters that LaTeX treats as special characters, which
2190 # means that they do not simply typeset themselves:
2191 # # $ % & ~ _ ^ \ { }
2193 # TeX ignores blanks following an escaped character; if you want a blank (as
2194 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2198 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2199 $value =~ s/([<>])/\$$1\$/g;
2203 #utility methods for print_*
2205 sub balance_due_msg {
2207 my $msg = 'Balance Due';
2208 return $msg unless $conf->exists('invoice_default_terms');
2209 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2210 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2211 } elsif ( $conf->config('invoice_default_terms') ) {
2212 $msg .= ' - '. $conf->config('invoice_default_terms');
2219 my @display = scalar(@_)
2221 : qw( _items_previous _items_pkg );
2222 #: qw( _items_pkg );
2223 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2225 foreach my $display ( @display ) {
2226 push @b, $self->$display(@_);
2231 sub _items_previous {
2233 my $cust_main = $self->cust_main;
2234 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2236 foreach ( @pr_cust_bill ) {
2238 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2239 ' ('. time2str('%x',$_->_date). ')',
2240 #'pkgpart' => 'N/A',
2242 'amount' => sprintf("%.2f", $_->owed),
2248 # 'description' => 'Previous Balance',
2249 # #'pkgpart' => 'N/A',
2250 # 'pkgnum' => 'N/A',
2251 # 'amount' => sprintf("%10.2f", $pr_total ),
2252 # 'ext_description' => [ map {
2253 # "Invoice ". $_->invnum.
2254 # " (". time2str("%x",$_->_date). ") ".
2255 # sprintf("%10.2f", $_->owed)
2256 # } @pr_cust_bill ],
2263 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2264 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2269 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2270 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2273 sub _items_cust_bill_pkg {
2275 my $cust_bill_pkg = shift;
2278 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2280 my $desc = $cust_bill_pkg->desc;
2282 if ( $cust_bill_pkg->pkgnum > 0 ) {
2284 if ( $cust_bill_pkg->setup != 0 ) {
2285 my $description = $desc;
2286 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2287 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2288 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2290 description => $description,
2291 #pkgpart => $part_pkg->pkgpart,
2292 pkgnum => $cust_bill_pkg->pkgnum,
2293 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2294 ext_description => \@d,
2298 if ( $cust_bill_pkg->recur != 0 ) {
2300 description => "$desc (" .
2301 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2302 time2str('%x', $cust_bill_pkg->edate). ')',
2303 #pkgpart => $part_pkg->pkgpart,
2304 pkgnum => $cust_bill_pkg->pkgnum,
2305 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2307 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2308 $cust_bill_pkg->sdate),
2309 $cust_bill_pkg->details,
2314 } else { #pkgnum tax or one-shot line item (??)
2316 if ( $cust_bill_pkg->setup != 0 ) {
2318 'description' => $desc,
2319 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2322 if ( $cust_bill_pkg->recur != 0 ) {
2324 'description' => "$desc (".
2325 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2326 time2str("%x", $cust_bill_pkg->edate). ')',
2327 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2339 sub _items_credits {
2344 foreach ( $self->cust_credited ) {
2346 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2348 my $reason = $_->cust_credit->reason;
2349 #my $reason = substr($_->cust_credit->reason,0,32);
2350 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2351 $reason = " ($reason) " if $reason;
2353 #'description' => 'Credit ref\#'. $_->crednum.
2354 # " (". time2str("%x",$_->cust_credit->_date) .")".
2356 'description' => 'Credit applied '.
2357 time2str("%x",$_->cust_credit->_date). $reason,
2358 'amount' => sprintf("%.2f",$_->amount),
2361 #foreach ( @cr_cust_credit ) {
2363 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2364 # $money_char. sprintf("%10.2f",$_->credited)
2372 sub _items_payments {
2376 #get & print payments
2377 foreach ( $self->cust_bill_pay ) {
2379 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2382 'description' => "Payment received ".
2383 time2str("%x",$_->cust_pay->_date ),
2384 'amount' => sprintf("%.2f", $_->amount )
2402 sub process_reprint {
2403 process_re_X('print', @_);
2410 sub process_reemail {
2411 process_re_X('email', @_);
2419 process_re_X('fax', @_);
2422 use Storable qw(thaw);
2426 my( $method, $job ) = ( shift, shift );
2428 my $param = thaw(decode_base64(shift));
2429 warn Dumper($param) if $DEBUG;
2440 my($method, $job, %param ) = @_;
2441 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2443 #some false laziness w/search/cust_bill.html
2445 my $orderby = 'ORDER BY cust_bill._date';
2449 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2450 push @where, "cust_bill._date >= $1";
2452 if ( $param{'end'} =~ /^(\d+)$/ ) {
2453 push @where, "cust_bill._date < $1";
2455 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2456 push @where, "cust_main.agentnum = $1";
2460 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2461 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2462 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2463 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2465 push @where, "0 != $owed"
2468 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2471 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2473 my $addl_from = 'left join cust_main using ( custnum )';
2475 if ( $param{'newest_percust'} ) {
2476 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2477 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2478 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2481 my @cust_bill = qsearch( 'cust_bill',
2483 "$distinct cust_bill.*",
2489 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2490 foreach my $cust_bill ( @cust_bill ) {
2491 $cust_bill->$method();
2493 if ( $job ) { #progressbar foo
2495 if ( time - $min_sec > $last ) {
2496 my $error = $job->update_statustext(
2497 int( 100 * $num / scalar(@cust_bill) )
2499 die $error if $error;
2514 print_text formatting (and some logic :/) is in source, but needs to be
2515 slurped in from a file. Also number of lines ($=).
2519 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2520 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base