4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax );
16 use FS::Record qw( qsearch qsearchs );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
27 use FS::cust_bill_pay;
28 use FS::part_bill_event;
30 @ISA = qw( FS::cust_main_Mixin FS::Record );
34 #ask FS::UID to run this stuff for us later
35 FS::UID->install_callback( sub {
37 $money_char = $conf->config('money_char') || '$';
42 FS::cust_bill - Object methods for cust_bill records
48 $record = new FS::cust_bill \%hash;
49 $record = new FS::cust_bill { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
59 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
61 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
63 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
65 @cust_pay_objects = $cust_bill->cust_pay;
67 $tax_amount = $record->tax;
69 @lines = $cust_bill->print_text;
70 @lines = $cust_bill->print_text $time;
74 An FS::cust_bill object represents an invoice; a declaration that a customer
75 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
76 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
77 following fields are currently supported:
81 =item invnum - primary key (assigned automatically for new invoices)
83 =item custnum - customer (see L<FS::cust_main>)
85 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
86 L<Time::Local> and L<Date::Parse> for conversion functions.
88 =item charged - amount of this invoice
90 =item printed - deprecated
92 =item closed - books closed flag, empty or `Y'
102 Creates a new invoice. To add the invoice to the database, see L<"insert">.
103 Invoices are normally created by calling the bill method of a customer object
104 (see L<FS::cust_main>).
108 sub table { 'cust_bill'; }
110 sub cust_linked { $_[0]->cust_main_custnum; }
111 sub cust_unlinked_msg {
113 "WARNING: can't find cust_main.custnum ". $self->custnum.
114 ' (cust_bill.invnum '. $self->invnum. ')';
119 Adds this invoice to the database ("Posts" the invoice). If there is an error,
120 returns the error, otherwise returns false.
124 Currently unimplemented. I don't remove invoices because there would then be
125 no record you ever posted this invoice (which is bad, no?)
131 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
132 $self->SUPER::delete(@_);
135 =item replace OLD_RECORD
137 Replaces the OLD_RECORD with this one in the database. If there is an error,
138 returns the error, otherwise returns false.
140 Only printed may be changed. printed is normally updated by calling the
141 collect method of a customer object (see L<FS::cust_main>).
146 my( $new, $old ) = ( shift, shift );
147 return "Can't change custnum!" unless $old->custnum == $new->custnum;
148 #return "Can't change _date!" unless $old->_date eq $new->_date;
149 return "Can't change _date!" unless $old->_date == $new->_date;
150 return "Can't change charged!" unless $old->charged == $new->charged;
152 $new->SUPER::replace($old);
157 Checks all fields to make sure this is a valid invoice. If there is an error,
158 returns the error, otherwise returns false. Called by the insert and replace
167 $self->ut_numbern('invnum')
168 || $self->ut_number('custnum')
169 || $self->ut_numbern('_date')
170 || $self->ut_money('charged')
171 || $self->ut_numbern('printed')
172 || $self->ut_enum('closed', [ '', 'Y' ])
174 return $error if $error;
176 return "Unknown customer"
177 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
179 $self->_date(time) unless $self->_date;
181 $self->printed(0) if $self->printed eq '';
188 Returns a list consisting of the total previous balance for this customer,
189 followed by the previous outstanding invoices (as FS::cust_bill objects also).
196 my @cust_bill = sort { $a->_date <=> $b->_date }
197 grep { $_->owed != 0 && $_->_date < $self->_date }
198 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
200 foreach ( @cust_bill ) { $total += $_->owed; }
206 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
212 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
215 =item cust_bill_event
217 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
222 sub cust_bill_event {
224 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
230 Returns the customer (see L<FS::cust_main>) for this invoice.
236 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
241 Depreciated. See the cust_credited method.
243 #Returns a list consisting of the total previous credited (see
244 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
245 #outstanding credits (FS::cust_credit objects).
251 croak "FS::cust_bill->cust_credit depreciated; see ".
252 "FS::cust_bill->cust_credit_bill";
255 #my @cust_credit = sort { $a->_date <=> $b->_date }
256 # grep { $_->credited != 0 && $_->_date < $self->_date }
257 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
259 #foreach (@cust_credit) { $total += $_->credited; }
260 #$total, @cust_credit;
265 Depreciated. See the cust_bill_pay method.
267 #Returns all payments (see L<FS::cust_pay>) for this invoice.
273 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
275 #sort { $a->_date <=> $b->_date }
276 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
282 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
288 sort { $a->_date <=> $b->_date }
289 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
294 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
300 sort { $a->_date <=> $b->_date }
301 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
307 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
314 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
316 foreach (@taxlines) { $total += $_->setup; }
322 Returns the amount owed (still outstanding) on this invoice, which is charged
323 minus all payment applications (see L<FS::cust_bill_pay>) and credit
324 applications (see L<FS::cust_credit_bill>).
330 my $balance = $self->charged;
331 $balance -= $_->amount foreach ( $self->cust_bill_pay );
332 $balance -= $_->amount foreach ( $self->cust_credited );
333 $balance = sprintf( "%.2f", $balance);
334 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
339 =item generate_email PARAMHASH
341 PARAMHASH can contain the following:
345 =item from => sender address, required
347 =item tempate => alternate template name, optional
349 =item print_text => text attachment arrayref, optional
351 =item subject => email subject, optional
355 Returns an argument list to be passed to L<FS::Misc::send_email>.
366 my $me = '[FS::cust_bill::generate_email]';
369 'from' => $args{'from'},
370 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
373 if (ref($args{'to'} eq 'ARRAY')) {
374 $return{'to'} = $args{'to'};
376 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
377 $self->cust_main->invoicing_list
381 if ( $conf->exists('invoice_html') ) {
383 warn "$me creating HTML/text multipart message"
386 $return{'nobody'} = 1;
388 my $alternative = build MIME::Entity
389 'Type' => 'multipart/alternative',
390 'Encoding' => '7bit',
391 'Disposition' => 'inline'
395 if ( $conf->exists('invoice_email_pdf')
396 and scalar($conf->config('invoice_email_pdf_note')) ) {
398 warn "$me using 'invoice_email_pdf_note' in multipart message"
400 $data = [ map { $_ . "\n" }
401 $conf->config('invoice_email_pdf_note')
406 warn "$me not using 'invoice_email_pdf_note' in multipart message"
408 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
409 $data = $args{'print_text'};
411 $data = [ $self->print_text('', $args{'template'}) ];
416 $alternative->attach(
417 'Type' => 'text/plain',
418 #'Encoding' => 'quoted-printable',
419 'Encoding' => '7bit',
421 'Disposition' => 'inline',
424 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
425 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
427 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
429 if ( defined($args{'_template'}) && length($args{'_template'})
430 && -e "$path/logo_". $args{'_template'}. ".png"
433 $file = "$path/logo_". $args{'_template'}. ".png";
435 $file = "$path/logo.png";
438 my $image = build MIME::Entity
439 'Type' => 'image/png',
440 'Encoding' => 'base64',
442 'Filename' => 'logo.png',
443 'Content-ID' => "<$content_id>",
446 $alternative->attach(
447 'Type' => 'text/html',
448 'Encoding' => 'quoted-printable',
449 'Data' => [ '<html>',
452 ' '. encode_entities($return{'subject'}),
455 ' <body bgcolor="#e8e8e8">',
456 $self->print_html('', $args{'template'}, $content_id),
460 'Disposition' => 'inline',
461 #'Filename' => 'invoice.pdf',
464 if ( $conf->exists('invoice_email_pdf') ) {
469 # multipart/alternative
475 my $related = build MIME::Entity 'Type' => 'multipart/related',
476 'Encoding' => '7bit';
478 #false laziness w/Misc::send_email
479 $related->head->replace('Content-type',
481 '; boundary="'. $related->head->multipart_boundary. '"'.
482 '; type=multipart/alternative'
485 $related->add_part($alternative);
487 $related->add_part($image);
489 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
491 $return{'mimeparts'} = [ $related, $pdf ];
495 #no other attachment:
497 # multipart/alternative
502 $return{'content-type'} = 'multipart/related';
503 $return{'mimeparts'} = [ $alternative, $image ];
504 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
505 #$return{'disposition'} = 'inline';
511 if ( $conf->exists('invoice_email_pdf') ) {
512 warn "$me creating PDF attachment"
515 #mime parts arguments a la MIME::Entity->build().
516 $return{'mimeparts'} = [
517 { $self->mimebuild_pdf('', $args{'template'}) }
521 if ( $conf->exists('invoice_email_pdf')
522 and scalar($conf->config('invoice_email_pdf_note')) ) {
524 warn "$me using 'invoice_email_pdf_note'"
526 $return{'body'} = [ map { $_ . "\n" }
527 $conf->config('invoice_email_pdf_note')
532 warn "$me not using 'invoice_email_pdf_note'"
534 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
535 $return{'body'} = $args{'print_text'};
537 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
550 Returns a list suitable for passing to MIME::Entity->build(), representing
551 this invoice as PDF attachment.
558 'Type' => 'application/pdf',
559 'Encoding' => 'base64',
560 'Data' => [ $self->print_pdf(@_) ],
561 'Disposition' => 'attachment',
562 'Filename' => 'invoice.pdf',
566 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
568 Sends this invoice to the destinations configured for this customer: sends
569 email, prints and/or faxes. See L<FS::cust_main_invoice>.
571 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
573 AGENTNUM, if specified, means that this invoice will only be sent for customers
574 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
575 single agent) or an arrayref of agentnums.
577 INVOICE_FROM, if specified, overrides the default email invoice From: address.
583 my $template = scalar(@_) ? shift : '';
584 if ( scalar(@_) && $_[0] ) {
585 my $agentnums = ref($_[0]) ? shift : [ shift ];
586 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
592 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
594 my @invoicing_list = $self->cust_main->invoicing_list;
596 $self->email($template, $invoice_from)
597 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
599 $self->print($template)
600 if grep { $_ eq 'POST' } @invoicing_list; #postal
602 $self->fax($template)
603 if grep { $_ eq 'FAX' } @invoicing_list; #fax
609 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
613 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
615 INVOICE_FROM, if specified, overrides the default email invoice From: address.
621 my $template = scalar(@_) ? shift : '';
625 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
627 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
628 $self->cust_main->invoicing_list;
630 #better to notify this person than silence
631 @invoicing_list = ($invoice_from) unless @invoicing_list;
633 my $error = send_email(
634 $self->generate_email(
635 'from' => $invoice_from,
636 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
637 'template' => $template,
640 die "can't email invoice: $error\n" if $error;
641 #die "$error\n" if $error;
645 =item lpr_data [ TEMPLATENAME ]
647 Returns the postscript or plaintext for this invoice as an arrayref.
649 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
654 my( $self, $template) = @_;
655 $conf->exists('invoice_latex')
656 ? [ $self->print_ps('', $template) ]
657 : [ $self->print_text('', $template) ];
660 =item print [ TEMPLATENAME ]
664 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
670 my $template = scalar(@_) ? shift : '';
672 my $lpr = $conf->config('lpr');
675 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
677 $outerr = ": $outerr" if length($outerr);
678 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
683 =item fax [ TEMPLATENAME ]
687 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
693 my $template = scalar(@_) ? shift : '';
695 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
696 unless $conf->exists('invoice_latex');
698 my $dialstring = $self->cust_main->getfield('fax');
701 my $error = send_fax( 'docdata' => $self->lpr_data($template),
702 'dialstring' => $dialstring,
704 die $error if $error;
708 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
710 Like B<send>, but only sends the invoice if it is the newest open invoice for
720 grep { $_->owed > 0 }
721 qsearch('cust_bill', {
722 'custnum' => $self->custnum,
723 #'_date' => { op=>'>', value=>$self->_date },
724 'invnum' => { op=>'>', value=>$self->invnum },
731 =item send_csv OPTION => VALUE, ...
733 Sends invoice as a CSV data-file to a remote host with the specified protocol.
737 protocol - currently only "ftp"
742 format - 'default' or 'billco'
745 If I<format> is not specified or "default", the file will be named
746 "N-YYYYMMDDHHMMSS.csv" where N is the invoice number and YYMMDDHHMMSS is a
750 If I<format> is "billco", two files will be created and uploaded. They will be named "N-YYYYMMDDHHMMSS-header.csv" and "N-YYYYMMDDHHMMSS-detail.csv" where N
751 is the invoice number and YYMMDDHHMMSS is a timestamp(???).
753 See L</print_csv> for a description of the output format.
758 my($self, %opt) = @_;
762 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
763 mkdir $spooldir, 0700 unless -d $spooldir;
765 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
766 my $file = "$spooldir/$tracctnum";
767 if ( lc($opt{'format'}) eq 'billco' ) {
768 $file .= '-header.csv';
770 #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
774 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
776 open(CSV, ">$file") or die "can't open $file: $!";
780 if ( lc($opt{'format'}) eq 'billco' ) {
783 $file = "$spooldir/$tracctnum-detail.csv";
784 open(CSV,">$file") or die "can't open $file: $!";
792 if ( $opt{protocol} eq 'ftp' ) {
793 eval "use Net::FTP;";
795 $net = Net::FTP->new($opt{server}) or die @$;
797 die "unknown protocol: $opt{protocol}";
800 $net->login( $opt{username}, $opt{password} )
801 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
803 $net->binary or die "can't set binary mode";
805 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
808 $net->put($oldfile) or die "can't put $oldfile: $!";
810 $net->put($file) or die "can't put $file: $!";
814 unlink $oldfile if $oldfile;
821 Spools CSV invoice data.
827 =item format - 'default' or 'billco'
829 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
836 my($self, %opt) = @_;
838 if ( $opt{'dest'} ) {
839 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
840 $self->cust_main->invoicing_list;
841 return 'N/A' unless $invoicing_list{$opt{'dest'}}
842 || ! keys %invoicing_list;
847 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
848 mkdir $spooldir, 0700 unless -d $spooldir;
850 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
851 my $file = "$spooldir/spool";
852 if ( lc($opt{'format'}) eq 'billco' ) {
853 $file .= '-header.csv';
855 #$file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
859 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
861 open(CSV, ">>$file") or die "can't open $file: $!";
868 if ( lc($opt{'format'}) eq 'billco' ) {
874 $file = "$spooldir/spool-detail.csv";
876 open(CSV,">>$file") or die "can't open $file: $!";
890 =item print_csv OPTION => VALUE, ...
892 Returns CSV data for this invoice.
896 format - 'default' or 'billco'
898 Returns a list consisting of two scalars. The first is a single line of CSV
899 header information for this invoice. The second is one or more lines of CSV
900 detail information for this invoice.
902 If I<format> is not specified or "default", the fields of the CSV file are as
905 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
909 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
911 B<record_type> is C<cust_bill> for the initial header line only. The
912 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
913 fields are filled in.
915 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
916 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
919 =item invnum - invoice number
921 =item custnum - customer number
923 =item _date - invoice date
925 =item charged - total invoice amount
927 =item first - customer first name
929 =item last - customer first name
931 =item company - company name
933 =item address1 - address line 1
935 =item address2 - address line 1
945 =item pkg - line item description
947 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
949 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
951 =item sdate - start date for recurring fee
953 =item edate - end date for recurring fee
957 If I<format> is "billco", the fields of the header CSV file are as follows:
959 +-------------------------------------------------------------------+
960 | FORMAT HEADER FILE |
961 |-------------------------------------------------------------------|
962 | Field | Description | Name | Type | Width |
963 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
964 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
965 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
966 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
967 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
968 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
969 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
970 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
971 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
972 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
973 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
974 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
975 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
976 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
977 | 15 | Previous Balance | BALFWD | NUM* | 9 |
978 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
979 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
980 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
981 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
982 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
983 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
984 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
985 | 23 | Y/N | AGESWITCH | CHAR | 1 |
986 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
987 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
988 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
989 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
990 | 28 | State Tax*** | STATETAX | NUM* | 9 |
991 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
992 +-------+-------------------------------+------------+------+-------+
994 If I<format> is "billco", the fields of the detail CSV file are as follows:
996 FORMAT FOR DETAIL FILE
998 Field | Description | Name | Type | Width
999 1 | N/A-Leave Empty | RC | CHAR | 2
1000 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1001 3 | Account Number | TRACCTNUM | CHAR | 15
1002 4 | Invoice Number | TRINVOICE | CHAR | 15
1003 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1004 6 | Transaction Detail | DETAILS | CHAR | 100
1005 7 | Amount | AMT | NUM* | 9
1006 8 | Line Format Control** | LNCTRL | CHAR | 2
1007 9 | Grouping Code | GROUP | CHAR | 2
1008 10 | User Defined | ACCT CODE | CHAR | 15
1013 my($self, %opt) = @_;
1015 eval "use Text::CSV_XS";
1018 my $cust_main = $self->cust_main;
1020 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1022 if ( lc($opt{'format'}) eq 'billco' ) {
1025 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1028 if ( $conf->exists('invoice_default_terms')
1029 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1030 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1033 my( $previous_balance, @unused ) = $self->previous; #previous balance
1035 my $pmt_cr_applied = 0;
1036 $pmt_cr_applied += $_->{'amount'}
1037 foreach ( $self->_items_payments, $self->_items_credits ) ;
1039 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1042 '', # 1 | N/A-Leave Empty CHAR 2
1043 '', # 2 | N/A-Leave Empty CHAR 15
1044 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1045 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1046 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1047 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1048 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1049 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1050 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1051 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1052 '', # 10 | Ancillary Billing Information CHAR 30
1053 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1054 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1057 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1060 $duedate, # 14 | Bill Due Date CHAR 10
1062 $previous_balance, # 15 | Previous Balance NUM* 9
1063 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1064 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1065 $totaldue, # 18 | Total Amt Due NUM* 9
1066 $totaldue, # 19 | Total Amt Due NUM* 9
1067 '', # 20 | 30 Day Aging NUM* 9
1068 '', # 21 | 60 Day Aging NUM* 9
1069 '', # 22 | 90 Day Aging NUM* 9
1070 'N', # 23 | Y/N CHAR 1
1071 '', # 24 | Remittance automation CHAR 100
1072 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1073 $self->custnum, # 26 | Customer Reference Number CHAR 15
1074 '0', # 27 | Federal Tax*** NUM* 9
1075 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1076 '0', # 29 | Other Taxes & Fees*** NUM* 9
1085 time2str("%x", $self->_date),
1086 sprintf("%.2f", $self->charged),
1087 ( map { $cust_main->getfield($_) }
1088 qw( first last company address1 address2 city state zip country ) ),
1090 ) or die "can't create csv";
1093 my $header = $csv->string. "\n";
1096 if ( lc($opt{'format'}) eq 'billco' ) {
1099 foreach my $item ( $self->_items_pkg ) {
1102 '', # 1 | N/A-Leave Empty CHAR 2
1103 '', # 2 | N/A-Leave Empty CHAR 15
1104 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1105 $self->invnum, # 4 | Invoice Number CHAR 15
1106 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1107 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1108 $item->{'amount'}, # 7 | Amount NUM* 9
1109 '', # 8 | Line Format Control** CHAR 2
1110 '', # 9 | Grouping Code CHAR 2
1111 '', # 10 | User Defined CHAR 15
1114 $detail .= $csv->string. "\n";
1120 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1122 my($pkg, $setup, $recur, $sdate, $edate);
1123 if ( $cust_bill_pkg->pkgnum ) {
1125 ($pkg, $setup, $recur, $sdate, $edate) = (
1126 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1127 ( $cust_bill_pkg->setup != 0
1128 ? sprintf("%.2f", $cust_bill_pkg->setup )
1130 ( $cust_bill_pkg->recur != 0
1131 ? sprintf("%.2f", $cust_bill_pkg->recur )
1133 ( $cust_bill_pkg->sdate
1134 ? time2str("%x", $cust_bill_pkg->sdate)
1136 ($cust_bill_pkg->edate
1137 ?time2str("%x", $cust_bill_pkg->edate)
1141 } else { #pkgnum tax
1142 next unless $cust_bill_pkg->setup != 0;
1143 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1144 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1146 ($pkg, $setup, $recur, $sdate, $edate) =
1147 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1153 ( map { '' } (1..11) ),
1154 ($pkg, $setup, $recur, $sdate, $edate)
1155 ) or die "can't create csv";
1157 $detail .= $csv->string. "\n";
1163 ( $header, $detail );
1169 Pays this invoice with a compliemntary payment. If there is an error,
1170 returns the error, otherwise returns false.
1176 my $cust_pay = new FS::cust_pay ( {
1177 'invnum' => $self->invnum,
1178 'paid' => $self->owed,
1181 'payinfo' => $self->cust_main->payinfo,
1189 Attempts to pay this invoice with a credit card payment via a
1190 Business::OnlinePayment realtime gateway. See
1191 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1192 for supported processors.
1198 $self->realtime_bop( 'CC', @_ );
1203 Attempts to pay this invoice with an electronic check (ACH) payment via a
1204 Business::OnlinePayment realtime gateway. See
1205 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1206 for supported processors.
1212 $self->realtime_bop( 'ECHECK', @_ );
1217 Attempts to pay this invoice with phone bill (LEC) payment via a
1218 Business::OnlinePayment realtime gateway. See
1219 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1220 for supported processors.
1226 $self->realtime_bop( 'LEC', @_ );
1230 my( $self, $method ) = @_;
1232 my $cust_main = $self->cust_main;
1233 my $balance = $cust_main->balance;
1234 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1235 $amount = sprintf("%.2f", $amount);
1236 return "not run (balance $balance)" unless $amount > 0;
1238 my $description = 'Internet Services';
1239 if ( $conf->exists('business-onlinepayment-description') ) {
1240 my $dtempl = $conf->config('business-onlinepayment-description');
1242 my $agent_obj = $cust_main->agent
1243 or die "can't retreive agent for $cust_main (agentnum ".
1244 $cust_main->agentnum. ")";
1245 my $agent = $agent_obj->agent;
1246 my $pkgs = join(', ',
1247 map { $_->cust_pkg->part_pkg->pkg }
1248 grep { $_->pkgnum } $self->cust_bill_pkg
1250 $description = eval qq("$dtempl");
1253 $cust_main->realtime_bop($method, $amount,
1254 'description' => $description,
1255 'invnum' => $self->invnum,
1262 Adds a payment for this invoice to the pending credit card batch (see
1263 L<FS::cust_pay_batch>).
1269 my $cust_main = $self->cust_main;
1271 my $cust_pay_batch = new FS::cust_pay_batch ( {
1272 'invnum' => $self->getfield('invnum'),
1273 'custnum' => $cust_main->getfield('custnum'),
1274 'last' => $cust_main->getfield('last'),
1275 'first' => $cust_main->getfield('first'),
1276 'address1' => $cust_main->getfield('address1'),
1277 'address2' => $cust_main->getfield('address2'),
1278 'city' => $cust_main->getfield('city'),
1279 'state' => $cust_main->getfield('state'),
1280 'zip' => $cust_main->getfield('zip'),
1281 'country' => $cust_main->getfield('country'),
1282 'cardnum' => $cust_main->payinfo,
1283 'exp' => $cust_main->getfield('paydate'),
1284 'payname' => $cust_main->getfield('payname'),
1285 'amount' => $self->owed,
1287 my $error = $cust_pay_batch->insert;
1288 die $error if $error;
1293 sub _agent_template {
1295 $self->_agent_plandata('agent_templatename');
1298 sub _agent_invoice_from {
1300 $self->_agent_plandata('agent_invoice_from');
1303 sub _agent_plandata {
1304 my( $self, $option ) = @_;
1306 my $part_bill_event = qsearchs( 'part_bill_event',
1308 'payby' => $self->cust_main->payby,
1309 'plan' => 'send_agent',
1310 'plandata' => { 'op' => '~',
1311 'value' => "(^|\n)agentnum ".
1313 $self->cust_main->agentnum.
1319 'ORDER BY seconds LIMIT 1'
1322 return '' unless $part_bill_event;
1324 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1327 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1328 " plandata for $option";
1334 =item print_text [ TIME [ , TEMPLATE ] ]
1336 Returns an text invoice, as a list of lines.
1338 TIME an optional value used to control the printing of overdue messages. The
1339 default is now. It isn't the date of the invoice; that's the `_date' field.
1340 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1341 L<Time::Local> and L<Date::Parse> for conversion functions.
1345 #still some false laziness w/_items stuff (and send_csv)
1348 my( $self, $today, $template ) = @_;
1351 # my $invnum = $self->invnum;
1352 my $cust_main = $self->cust_main;
1353 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1354 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1356 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1357 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1358 #my $balance_due = $self->owed + $pr_total - $cr_total;
1359 my $balance_due = $self->owed + $pr_total;
1362 #my($description,$amount);
1366 foreach ( @pr_cust_bill ) {
1368 "Previous Balance, Invoice #". $_->invnum.
1369 " (". time2str("%x",$_->_date). ")",
1370 $money_char. sprintf("%10.2f",$_->owed)
1373 if (@pr_cust_bill) {
1374 push @buf,['','-----------'];
1375 push @buf,[ 'Total Previous Balance',
1376 $money_char. sprintf("%10.2f",$pr_total ) ];
1381 foreach my $cust_bill_pkg (
1382 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1383 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1386 my $desc = $cust_bill_pkg->desc;
1388 if ( $cust_bill_pkg->pkgnum > 0 ) {
1390 if ( $cust_bill_pkg->setup != 0 ) {
1391 my $description = $desc;
1392 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1393 push @buf, [ $description,
1394 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1396 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1397 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1400 if ( $cust_bill_pkg->recur != 0 ) {
1402 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1403 time2str("%x", $cust_bill_pkg->edate) . ")",
1404 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1407 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1408 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1409 $cust_bill_pkg->sdate );
1412 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1414 } else { #pkgnum tax or one-shot line item
1416 if ( $cust_bill_pkg->setup != 0 ) {
1418 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1420 if ( $cust_bill_pkg->recur != 0 ) {
1421 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1422 . time2str("%x", $cust_bill_pkg->edate). ")",
1423 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1431 push @buf,['','-----------'];
1432 push @buf,['Total New Charges',
1433 $money_char. sprintf("%10.2f",$self->charged) ];
1436 push @buf,['','-----------'];
1437 push @buf,['Total Charges',
1438 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1442 foreach ( $self->cust_credited ) {
1444 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1446 my $reason = substr($_->cust_credit->reason,0,32);
1447 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1448 $reason = " ($reason) " if $reason;
1450 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1452 $money_char. sprintf("%10.2f",$_->amount)
1455 #foreach ( @cr_cust_credit ) {
1457 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1458 # $money_char. sprintf("%10.2f",$_->credited)
1462 #get & print payments
1463 foreach ( $self->cust_bill_pay ) {
1465 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1468 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1469 $money_char. sprintf("%10.2f",$_->amount )
1474 my $balance_due_msg = $self->balance_due_msg;
1476 push @buf,['','-----------'];
1477 push @buf,[$balance_due_msg, $money_char.
1478 sprintf("%10.2f", $balance_due ) ];
1480 #create the template
1481 $template ||= $self->_agent_template;
1482 my $templatefile = 'invoice_template';
1483 $templatefile .= "_$template" if length($template);
1484 my @invoice_template = $conf->config($templatefile)
1485 or die "cannot load config file $templatefile";
1488 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1489 /invoice_lines\((\d*)\)/;
1490 $invoice_lines += $1 || scalar(@buf);
1493 die "no invoice_lines() functions in template?" unless $wasfunc;
1494 my $invoice_template = new Text::Template (
1496 SOURCE => [ map "$_\n", @invoice_template ],
1497 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1498 $invoice_template->compile()
1499 or die "can't compile template: $Text::Template::ERROR";
1501 #setup template variables
1502 package FS::cust_bill::_template; #!
1503 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1505 $invnum = $self->invnum;
1506 $date = $self->_date;
1508 $agent = $self->cust_main->agent->agent;
1510 if ( $FS::cust_bill::invoice_lines ) {
1512 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1514 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1519 #format address (variable for the template)
1521 @address = ( '', '', '', '', '', '' );
1522 package FS::cust_bill; #!
1523 $FS::cust_bill::_template::address[$l++] =
1524 $cust_main->payname.
1525 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1526 ? " (P.O. #". $cust_main->payinfo. ")"
1530 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1531 if $cust_main->company;
1532 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1533 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1534 if $cust_main->address2;
1535 $FS::cust_bill::_template::address[$l++] =
1536 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1538 my $countrydefault = $conf->config('countrydefault') || 'US';
1539 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1540 unless $cust_main->country eq $countrydefault;
1542 # #overdue? (variable for the template)
1543 # $FS::cust_bill::_template::overdue = (
1545 # && $today > $self->_date
1546 ## && $self->printed > 1
1547 # && $self->printed > 0
1550 #and subroutine for the template
1551 sub FS::cust_bill::_template::invoice_lines {
1552 my $lines = shift || scalar(@buf);
1554 scalar(@buf) ? shift @buf : [ '', '' ];
1560 $FS::cust_bill::_template::page = 1;
1564 push @collect, split("\n",
1565 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1567 $FS::cust_bill::_template::page++;
1570 map "$_\n", @collect;
1574 =item print_latex [ TIME [ , TEMPLATE ] ]
1576 Internal method - returns a filename of a filled-in LaTeX template for this
1577 invoice (Note: add ".tex" to get the actual filename).
1579 See print_ps and print_pdf for methods that return PostScript and PDF output.
1581 TIME an optional value used to control the printing of overdue messages. The
1582 default is now. It isn't the date of the invoice; that's the `_date' field.
1583 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1584 L<Time::Local> and L<Date::Parse> for conversion functions.
1588 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1591 my( $self, $today, $template ) = @_;
1593 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1596 my $cust_main = $self->cust_main;
1597 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1598 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1600 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1601 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1602 #my $balance_due = $self->owed + $pr_total - $cr_total;
1603 my $balance_due = $self->owed + $pr_total;
1605 #create the template
1606 $template ||= $self->_agent_template;
1607 my $templatefile = 'invoice_latex';
1608 my $suffix = length($template) ? "_$template" : '';
1609 $templatefile .= $suffix;
1610 my @invoice_template = map "$_\n", $conf->config($templatefile)
1611 or die "cannot load config file $templatefile";
1613 my($format, $text_template);
1614 if ( grep { /^%%Detail/ } @invoice_template ) {
1615 #change this to a die when the old code is removed
1616 warn "old-style invoice template $templatefile; ".
1617 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1620 $format = 'Text::Template';
1621 $text_template = new Text::Template(
1623 SOURCE => \@invoice_template,
1624 DELIMITERS => [ '[@--', '--@]' ],
1627 $text_template->compile()
1628 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1632 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1633 $returnaddress = join("\n",
1634 $conf->config_orbase('invoice_latexreturnaddress', $template)
1637 $returnaddress = '~';
1640 my %invoice_data = (
1641 'invnum' => $self->invnum,
1642 'date' => time2str('%b %o, %Y', $self->_date),
1643 'today' => time2str('%b %o, %Y', $today),
1644 'agent' => _latex_escape($cust_main->agent->agent),
1645 'payname' => _latex_escape($cust_main->payname),
1646 'company' => _latex_escape($cust_main->company),
1647 'address1' => _latex_escape($cust_main->address1),
1648 'address2' => _latex_escape($cust_main->address2),
1649 'city' => _latex_escape($cust_main->city),
1650 'state' => _latex_escape($cust_main->state),
1651 'zip' => _latex_escape($cust_main->zip),
1652 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1653 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1654 'returnaddress' => $returnaddress,
1656 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1657 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1658 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1661 my $countrydefault = $conf->config('countrydefault') || 'US';
1662 if ( $cust_main->country eq $countrydefault ) {
1663 $invoice_data{'country'} = '';
1665 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1668 $invoice_data{'notes'} =
1670 # #do variable substitutions in notes
1671 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1672 $conf->config_orbase('invoice_latexnotes', $template)
1674 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1677 $invoice_data{'footer'} =~ s/\n+$//;
1678 $invoice_data{'smallfooter'} =~ s/\n+$//;
1679 $invoice_data{'notes'} =~ s/\n+$//;
1681 $invoice_data{'po_line'} =
1682 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1683 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1687 if ( $format eq 'old' ) {
1690 my @total_item = ();
1691 while ( @invoice_template ) {
1692 my $line = shift @invoice_template;
1694 if ( $line =~ /^%%Detail\s*$/ ) {
1696 while ( ( my $line_item_line = shift @invoice_template )
1697 !~ /^%%EndDetail\s*$/ ) {
1698 push @line_item, $line_item_line;
1700 foreach my $line_item ( $self->_items ) {
1701 #foreach my $line_item ( $self->_items_pkg ) {
1702 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1703 $invoice_data{'description'} =
1704 _latex_escape($line_item->{'description'});
1705 if ( exists $line_item->{'ext_description'} ) {
1706 $invoice_data{'description'} .=
1707 "\\tabularnewline\n~~".
1708 join( "\\tabularnewline\n~~",
1709 map _latex_escape($_), @{$line_item->{'ext_description'}}
1712 $invoice_data{'amount'} = $line_item->{'amount'};
1713 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1715 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1718 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1720 while ( ( my $total_item_line = shift @invoice_template )
1721 !~ /^%%EndTotalDetails\s*$/ ) {
1722 push @total_item, $total_item_line;
1725 my @total_fill = ();
1728 foreach my $tax ( $self->_items_tax ) {
1729 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1730 $taxtotal += $tax->{'amount'};
1731 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1733 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1738 $invoice_data{'total_item'} = 'Sub-total';
1739 $invoice_data{'total_amount'} =
1740 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1741 unshift @total_fill,
1742 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1746 $invoice_data{'total_item'} = '\textbf{Total}';
1747 $invoice_data{'total_amount'} =
1748 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1750 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1753 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1756 foreach my $credit ( $self->_items_credits ) {
1757 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1759 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1761 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1766 foreach my $payment ( $self->_items_payments ) {
1767 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1769 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1771 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1775 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1776 $invoice_data{'total_amount'} =
1777 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1779 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1782 push @filled_in, @total_fill;
1785 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1786 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1787 push @filled_in, $line;
1798 } elsif ( $format eq 'Text::Template' ) {
1800 my @detail_items = ();
1801 my @total_items = ();
1803 $invoice_data{'detail_items'} = \@detail_items;
1804 $invoice_data{'total_items'} = \@total_items;
1806 foreach my $line_item ( $self->_items ) {
1808 ext_description => [],
1810 $detail->{'ref'} = $line_item->{'pkgnum'};
1811 $detail->{'quantity'} = 1;
1812 $detail->{'description'} = _latex_escape($line_item->{'description'});
1813 if ( exists $line_item->{'ext_description'} ) {
1814 @{$detail->{'ext_description'}} = map {
1816 } @{$line_item->{'ext_description'}};
1818 $detail->{'amount'} = $line_item->{'amount'};
1819 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1821 push @detail_items, $detail;
1826 foreach my $tax ( $self->_items_tax ) {
1828 $total->{'total_item'} = _latex_escape($tax->{'description'});
1829 $taxtotal += $tax->{'amount'};
1830 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1831 push @total_items, $total;
1836 $total->{'total_item'} = 'Sub-total';
1837 $total->{'total_amount'} =
1838 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1839 unshift @total_items, $total;
1844 $total->{'total_item'} = '\textbf{Total}';
1845 $total->{'total_amount'} =
1846 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1847 push @total_items, $total;
1850 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1853 foreach my $credit ( $self->_items_credits ) {
1855 $total->{'total_item'} = _latex_escape($credit->{'description'});
1857 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1858 push @total_items, $total;
1862 foreach my $payment ( $self->_items_payments ) {
1864 $total->{'total_item'} = _latex_escape($payment->{'description'});
1866 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1867 push @total_items, $total;
1872 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1873 $total->{'total_amount'} =
1874 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1875 push @total_items, $total;
1879 die "guru meditation #54";
1882 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1883 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1887 ) or die "can't open temp file: $!\n";
1888 if ( $format eq 'old' ) {
1889 print $fh join('', @filled_in );
1890 } elsif ( $format eq 'Text::Template' ) {
1891 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1893 die "guru meditation #32";
1897 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1902 =item print_ps [ TIME [ , TEMPLATE ] ]
1904 Returns an postscript invoice, as a scalar.
1906 TIME an optional value used to control the printing of overdue messages. The
1907 default is now. It isn't the date of the invoice; that's the `_date' field.
1908 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1909 L<Time::Local> and L<Date::Parse> for conversion functions.
1916 my $file = $self->print_latex(@_);
1918 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1921 my $sfile = shell_quote $file;
1923 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1924 or die "pslatex $file.tex failed; see $file.log for details?\n";
1925 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1926 or die "pslatex $file.tex failed; see $file.log for details?\n";
1928 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1929 or die "dvips failed";
1931 open(POSTSCRIPT, "<$file.ps")
1932 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1934 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1937 while (<POSTSCRIPT>) {
1947 =item print_pdf [ TIME [ , TEMPLATE ] ]
1949 Returns an PDF invoice, as a scalar.
1951 TIME an optional value used to control the printing of overdue messages. The
1952 default is now. It isn't the date of the invoice; that's the `_date' field.
1953 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1954 L<Time::Local> and L<Date::Parse> for conversion functions.
1961 my $file = $self->print_latex(@_);
1963 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1966 #system('pdflatex', "$file.tex");
1967 #system('pdflatex', "$file.tex");
1968 #! LaTeX Error: Unknown graphics extension: .eps.
1970 my $sfile = shell_quote $file;
1972 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1973 or die "pslatex $file.tex failed; see $file.log for details?\n";
1974 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1975 or die "pslatex $file.tex failed; see $file.log for details?\n";
1977 #system('dvipdf', "$file.dvi", "$file.pdf" );
1979 "dvips -q -t letter -f $sfile.dvi ".
1980 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1983 or die "dvips | gs failed: $!";
1985 open(PDF, "<$file.pdf")
1986 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1988 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2001 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2003 Returns an HTML invoice, as a scalar.
2005 TIME an optional value used to control the printing of overdue messages. The
2006 default is now. It isn't the date of the invoice; that's the `_date' field.
2007 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2008 L<Time::Local> and L<Date::Parse> for conversion functions.
2010 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2011 when emailing the invoice as part of a multipart/related MIME email.
2015 #some falze laziness w/print_text and print_latex (and send_csv)
2017 my( $self, $today, $template, $cid ) = @_;
2020 my $cust_main = $self->cust_main;
2021 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2022 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2024 $template ||= $self->_agent_template;
2025 my $templatefile = 'invoice_html';
2026 my $suffix = length($template) ? "_$template" : '';
2027 $templatefile .= $suffix;
2028 my @html_template = map "$_\n", $conf->config($templatefile)
2029 or die "cannot load config file $templatefile";
2031 my $html_template = new Text::Template(
2033 SOURCE => \@html_template,
2034 DELIMITERS => [ '<%=', '%>' ],
2037 $html_template->compile()
2038 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2040 my %invoice_data = (
2041 'invnum' => $self->invnum,
2042 'date' => time2str('%b %o, %Y', $self->_date),
2043 'today' => time2str('%b %o, %Y', $today),
2044 'agent' => encode_entities($cust_main->agent->agent),
2045 'payname' => encode_entities($cust_main->payname),
2046 'company' => encode_entities($cust_main->company),
2047 'address1' => encode_entities($cust_main->address1),
2048 'address2' => encode_entities($cust_main->address2),
2049 'city' => encode_entities($cust_main->city),
2050 'state' => encode_entities($cust_main->state),
2051 'zip' => encode_entities($cust_main->zip),
2052 'terms' => $conf->config('invoice_default_terms')
2053 || 'Payable upon receipt',
2055 'template' => $template,
2056 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2060 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2061 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2063 $invoice_data{'returnaddress'} =
2064 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2066 $invoice_data{'returnaddress'} =
2069 s/\\\\\*?\s*$/<BR>/;
2070 s/\\hyphenation\{[\w\s\-]+\}//;
2073 $conf->config_orbase( 'invoice_latexreturnaddress',
2079 my $countrydefault = $conf->config('countrydefault') || 'US';
2080 if ( $cust_main->country eq $countrydefault ) {
2081 $invoice_data{'country'} = '';
2083 $invoice_data{'country'} =
2084 encode_entities(code2country($cust_main->country));
2088 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2089 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2091 $invoice_data{'notes'} =
2092 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2094 $invoice_data{'notes'} =
2096 s/%%(.*)$/<!-- $1 -->/;
2097 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2098 s/\\begin\{enumerate\}/<ol>/;
2100 s/\\end\{enumerate\}/<\/ol>/;
2101 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2104 $conf->config_orbase('invoice_latexnotes', $template)
2108 # #do variable substitutions in notes
2109 # $invoice_data{'notes'} =
2111 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2112 # $conf->config_orbase('invoice_latexnotes', $suffix)
2116 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2117 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2119 $invoice_data{'footer'} =
2120 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2122 $invoice_data{'footer'} =
2123 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2124 $conf->config_orbase('invoice_latexfooter', $template)
2128 $invoice_data{'po_line'} =
2129 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2130 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2133 my $money_char = $conf->config('money_char') || '$';
2135 foreach my $line_item ( $self->_items ) {
2137 ext_description => [],
2139 $detail->{'ref'} = $line_item->{'pkgnum'};
2140 $detail->{'description'} = encode_entities($line_item->{'description'});
2141 if ( exists $line_item->{'ext_description'} ) {
2142 @{$detail->{'ext_description'}} = map {
2143 encode_entities($_);
2144 } @{$line_item->{'ext_description'}};
2146 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2147 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2149 push @{$invoice_data{'detail_items'}}, $detail;
2154 foreach my $tax ( $self->_items_tax ) {
2156 $total->{'total_item'} = encode_entities($tax->{'description'});
2157 $taxtotal += $tax->{'amount'};
2158 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2159 push @{$invoice_data{'total_items'}}, $total;
2164 $total->{'total_item'} = 'Sub-total';
2165 $total->{'total_amount'} =
2166 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2167 unshift @{$invoice_data{'total_items'}}, $total;
2170 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2173 $total->{'total_item'} = '<b>Total</b>';
2174 $total->{'total_amount'} =
2175 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2176 push @{$invoice_data{'total_items'}}, $total;
2179 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2182 foreach my $credit ( $self->_items_credits ) {
2184 $total->{'total_item'} = encode_entities($credit->{'description'});
2186 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2187 push @{$invoice_data{'total_items'}}, $total;
2191 foreach my $payment ( $self->_items_payments ) {
2193 $total->{'total_item'} = encode_entities($payment->{'description'});
2195 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2196 push @{$invoice_data{'total_items'}}, $total;
2201 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2202 $total->{'total_amount'} =
2203 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2204 push @{$invoice_data{'total_items'}}, $total;
2207 $html_template->fill_in( HASH => \%invoice_data);
2210 # quick subroutine for print_latex
2212 # There are ten characters that LaTeX treats as special characters, which
2213 # means that they do not simply typeset themselves:
2214 # # $ % & ~ _ ^ \ { }
2216 # TeX ignores blanks following an escaped character; if you want a blank (as
2217 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2221 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2222 $value =~ s/([<>])/\$$1\$/g;
2226 #utility methods for print_*
2228 sub balance_due_msg {
2230 my $msg = 'Balance Due';
2231 return $msg unless $conf->exists('invoice_default_terms');
2232 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2233 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2234 } elsif ( $conf->config('invoice_default_terms') ) {
2235 $msg .= ' - '. $conf->config('invoice_default_terms');
2242 my @display = scalar(@_)
2244 : qw( _items_previous _items_pkg );
2245 #: qw( _items_pkg );
2246 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2248 foreach my $display ( @display ) {
2249 push @b, $self->$display(@_);
2254 sub _items_previous {
2256 my $cust_main = $self->cust_main;
2257 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2259 foreach ( @pr_cust_bill ) {
2261 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2262 ' ('. time2str('%x',$_->_date). ')',
2263 #'pkgpart' => 'N/A',
2265 'amount' => sprintf("%.2f", $_->owed),
2271 # 'description' => 'Previous Balance',
2272 # #'pkgpart' => 'N/A',
2273 # 'pkgnum' => 'N/A',
2274 # 'amount' => sprintf("%10.2f", $pr_total ),
2275 # 'ext_description' => [ map {
2276 # "Invoice ". $_->invnum.
2277 # " (". time2str("%x",$_->_date). ") ".
2278 # sprintf("%10.2f", $_->owed)
2279 # } @pr_cust_bill ],
2286 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2287 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2292 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2293 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2296 sub _items_cust_bill_pkg {
2298 my $cust_bill_pkg = shift;
2301 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2303 my $desc = $cust_bill_pkg->desc;
2305 if ( $cust_bill_pkg->pkgnum > 0 ) {
2307 if ( $cust_bill_pkg->setup != 0 ) {
2308 my $description = $desc;
2309 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2310 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2311 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2313 description => $description,
2314 #pkgpart => $part_pkg->pkgpart,
2315 pkgnum => $cust_bill_pkg->pkgnum,
2316 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2317 ext_description => \@d,
2321 if ( $cust_bill_pkg->recur != 0 ) {
2323 description => "$desc (" .
2324 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2325 time2str('%x', $cust_bill_pkg->edate). ')',
2326 #pkgpart => $part_pkg->pkgpart,
2327 pkgnum => $cust_bill_pkg->pkgnum,
2328 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2330 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2331 $cust_bill_pkg->sdate),
2332 $cust_bill_pkg->details,
2337 } else { #pkgnum tax or one-shot line item (??)
2339 if ( $cust_bill_pkg->setup != 0 ) {
2341 'description' => $desc,
2342 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2345 if ( $cust_bill_pkg->recur != 0 ) {
2347 'description' => "$desc (".
2348 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2349 time2str("%x", $cust_bill_pkg->edate). ')',
2350 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2362 sub _items_credits {
2367 foreach ( $self->cust_credited ) {
2369 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2371 my $reason = $_->cust_credit->reason;
2372 #my $reason = substr($_->cust_credit->reason,0,32);
2373 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2374 $reason = " ($reason) " if $reason;
2376 #'description' => 'Credit ref\#'. $_->crednum.
2377 # " (". time2str("%x",$_->cust_credit->_date) .")".
2379 'description' => 'Credit applied '.
2380 time2str("%x",$_->cust_credit->_date). $reason,
2381 'amount' => sprintf("%.2f",$_->amount),
2384 #foreach ( @cr_cust_credit ) {
2386 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2387 # $money_char. sprintf("%10.2f",$_->credited)
2395 sub _items_payments {
2399 #get & print payments
2400 foreach ( $self->cust_bill_pay ) {
2402 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2405 'description' => "Payment received ".
2406 time2str("%x",$_->cust_pay->_date ),
2407 'amount' => sprintf("%.2f", $_->amount )
2425 sub process_reprint {
2426 process_re_X('print', @_);
2433 sub process_reemail {
2434 process_re_X('email', @_);
2442 process_re_X('fax', @_);
2445 use Storable qw(thaw);
2449 my( $method, $job ) = ( shift, shift );
2451 my $param = thaw(decode_base64(shift));
2452 warn Dumper($param) if $DEBUG;
2463 my($method, $job, %param ) = @_;
2464 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2466 #some false laziness w/search/cust_bill.html
2468 my $orderby = 'ORDER BY cust_bill._date';
2472 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2473 push @where, "cust_bill._date >= $1";
2475 if ( $param{'end'} =~ /^(\d+)$/ ) {
2476 push @where, "cust_bill._date < $1";
2478 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2479 push @where, "cust_main.agentnum = $1";
2483 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2484 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2485 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2486 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2488 push @where, "0 != $owed"
2491 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2494 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2496 my $addl_from = 'left join cust_main using ( custnum )';
2498 if ( $param{'newest_percust'} ) {
2499 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2500 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2501 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2504 my @cust_bill = qsearch( 'cust_bill',
2506 "$distinct cust_bill.*",
2512 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2513 foreach my $cust_bill ( @cust_bill ) {
2514 $cust_bill->$method();
2516 if ( $job ) { #progressbar foo
2518 if ( time - $min_sec > $last ) {
2519 my $error = $job->update_statustext(
2520 int( 100 * $num / scalar(@cust_bill) )
2522 die $error if $error;
2537 print_text formatting (and some logic :/) is in source, but needs to be
2538 slurped in from a file. Also number of lines ($=).
2542 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2543 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base