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 This method now works but you probably shouldn't use it. Instead, apply a
125 credit against the invoice.
127 Using this method to delete invoices outright is really, really bad. There
128 would be no record you ever posted this invoice, and there are no check to
129 make sure charged = 0 or that there are no associated cust_bill_pkg records.
131 Really, don't use it.
137 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
138 $self->SUPER::delete(@_);
141 =item replace OLD_RECORD
143 Replaces the OLD_RECORD with this one in the database. If there is an error,
144 returns the error, otherwise returns false.
146 Only printed may be changed. printed is normally updated by calling the
147 collect method of a customer object (see L<FS::cust_main>).
151 #replace can be inherited from Record.pm
153 # replace_check is now the preferred way to #implement replace data checks
154 # (so $object->replace() works without an argument)
157 my( $new, $old ) = ( shift, shift );
158 return "Can't change custnum!" unless $old->custnum == $new->custnum;
159 #return "Can't change _date!" unless $old->_date eq $new->_date;
160 return "Can't change _date!" unless $old->_date == $new->_date;
161 return "Can't change charged!" unless $old->charged == $new->charged
162 || $old->charged == 0;
169 Checks all fields to make sure this is a valid invoice. If there is an error,
170 returns the error, otherwise returns false. Called by the insert and replace
179 $self->ut_numbern('invnum')
180 || $self->ut_number('custnum')
181 || $self->ut_numbern('_date')
182 || $self->ut_money('charged')
183 || $self->ut_numbern('printed')
184 || $self->ut_enum('closed', [ '', 'Y' ])
186 return $error if $error;
188 return "Unknown customer"
189 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
191 $self->_date(time) unless $self->_date;
193 $self->printed(0) if $self->printed eq '';
200 Returns a list consisting of the total previous balance for this customer,
201 followed by the previous outstanding invoices (as FS::cust_bill objects also).
208 my @cust_bill = sort { $a->_date <=> $b->_date }
209 grep { $_->owed != 0 && $_->_date < $self->_date }
210 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
212 foreach ( @cust_bill ) { $total += $_->owed; }
218 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
224 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
227 =item cust_bill_event
229 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
234 sub cust_bill_event {
236 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
242 Returns the customer (see L<FS::cust_main>) for this invoice.
248 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
251 =item cust_suspend_if_balance_over AMOUNT
253 Suspends the customer associated with this invoice if the total amount owed on
254 this invoice and all older invoices is greater than the specified amount.
256 Returns a list: an empty list on success or a list of errors.
260 sub cust_suspend_if_balance_over {
261 my( $self, $amount ) = ( shift, shift );
262 my $cust_main = $self->cust_main;
263 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
272 Depreciated. See the cust_credited method.
274 #Returns a list consisting of the total previous credited (see
275 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
276 #outstanding credits (FS::cust_credit objects).
282 croak "FS::cust_bill->cust_credit depreciated; see ".
283 "FS::cust_bill->cust_credit_bill";
286 #my @cust_credit = sort { $a->_date <=> $b->_date }
287 # grep { $_->credited != 0 && $_->_date < $self->_date }
288 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
290 #foreach (@cust_credit) { $total += $_->credited; }
291 #$total, @cust_credit;
296 Depreciated. See the cust_bill_pay method.
298 #Returns all payments (see L<FS::cust_pay>) for this invoice.
304 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
306 #sort { $a->_date <=> $b->_date }
307 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
313 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
319 sort { $a->_date <=> $b->_date }
320 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
325 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
331 sort { $a->_date <=> $b->_date }
332 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
338 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
345 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
347 foreach (@taxlines) { $total += $_->setup; }
353 Returns the amount owed (still outstanding) on this invoice, which is charged
354 minus all payment applications (see L<FS::cust_bill_pay>) and credit
355 applications (see L<FS::cust_credit_bill>).
361 my $balance = $self->charged;
362 $balance -= $_->amount foreach ( $self->cust_bill_pay );
363 $balance -= $_->amount foreach ( $self->cust_credited );
364 $balance = sprintf( "%.2f", $balance);
365 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
370 =item generate_email PARAMHASH
372 PARAMHASH can contain the following:
376 =item from => sender address, required
378 =item tempate => alternate template name, optional
380 =item print_text => text attachment arrayref, optional
382 =item subject => email subject, optional
386 Returns an argument list to be passed to L<FS::Misc::send_email>.
397 my $me = '[FS::cust_bill::generate_email]';
400 'from' => $args{'from'},
401 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
404 if (ref($args{'to'} eq 'ARRAY')) {
405 $return{'to'} = $args{'to'};
407 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
408 $self->cust_main->invoicing_list
412 if ( $conf->exists('invoice_html') ) {
414 warn "$me creating HTML/text multipart message"
417 $return{'nobody'} = 1;
419 my $alternative = build MIME::Entity
420 'Type' => 'multipart/alternative',
421 'Encoding' => '7bit',
422 'Disposition' => 'inline'
426 if ( $conf->exists('invoice_email_pdf')
427 and scalar($conf->config('invoice_email_pdf_note')) ) {
429 warn "$me using 'invoice_email_pdf_note' in multipart message"
431 $data = [ map { $_ . "\n" }
432 $conf->config('invoice_email_pdf_note')
437 warn "$me not using 'invoice_email_pdf_note' in multipart message"
439 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
440 $data = $args{'print_text'};
442 $data = [ $self->print_text('', $args{'template'}) ];
447 $alternative->attach(
448 'Type' => 'text/plain',
449 #'Encoding' => 'quoted-printable',
450 'Encoding' => '7bit',
452 'Disposition' => 'inline',
455 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
456 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
458 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
460 if ( defined($args{'_template'}) && length($args{'_template'})
461 && -e "$path/logo_". $args{'_template'}. ".png"
464 $file = "$path/logo_". $args{'_template'}. ".png";
466 $file = "$path/logo.png";
469 my $image = build MIME::Entity
470 'Type' => 'image/png',
471 'Encoding' => 'base64',
473 'Filename' => 'logo.png',
474 'Content-ID' => "<$content_id>",
477 $alternative->attach(
478 'Type' => 'text/html',
479 'Encoding' => 'quoted-printable',
480 'Data' => [ '<html>',
483 ' '. encode_entities($return{'subject'}),
486 ' <body bgcolor="#e8e8e8">',
487 $self->print_html('', $args{'template'}, $content_id),
491 'Disposition' => 'inline',
492 #'Filename' => 'invoice.pdf',
495 if ( $conf->exists('invoice_email_pdf') ) {
500 # multipart/alternative
506 my $related = build MIME::Entity 'Type' => 'multipart/related',
507 'Encoding' => '7bit';
509 #false laziness w/Misc::send_email
510 $related->head->replace('Content-type',
512 '; boundary="'. $related->head->multipart_boundary. '"'.
513 '; type=multipart/alternative'
516 $related->add_part($alternative);
518 $related->add_part($image);
520 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
522 $return{'mimeparts'} = [ $related, $pdf ];
526 #no other attachment:
528 # multipart/alternative
533 $return{'content-type'} = 'multipart/related';
534 $return{'mimeparts'} = [ $alternative, $image ];
535 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
536 #$return{'disposition'} = 'inline';
542 if ( $conf->exists('invoice_email_pdf') ) {
543 warn "$me creating PDF attachment"
546 #mime parts arguments a la MIME::Entity->build().
547 $return{'mimeparts'} = [
548 { $self->mimebuild_pdf('', $args{'template'}) }
552 if ( $conf->exists('invoice_email_pdf')
553 and scalar($conf->config('invoice_email_pdf_note')) ) {
555 warn "$me using 'invoice_email_pdf_note'"
557 $return{'body'} = [ map { $_ . "\n" }
558 $conf->config('invoice_email_pdf_note')
563 warn "$me not using 'invoice_email_pdf_note'"
565 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
566 $return{'body'} = $args{'print_text'};
568 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
581 Returns a list suitable for passing to MIME::Entity->build(), representing
582 this invoice as PDF attachment.
589 'Type' => 'application/pdf',
590 'Encoding' => 'base64',
591 'Data' => [ $self->print_pdf(@_) ],
592 'Disposition' => 'attachment',
593 'Filename' => 'invoice.pdf',
597 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
599 Sends this invoice to the destinations configured for this customer: sends
600 email, prints and/or faxes. See L<FS::cust_main_invoice>.
602 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
604 AGENTNUM, if specified, means that this invoice will only be sent for customers
605 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
606 single agent) or an arrayref of agentnums.
608 INVOICE_FROM, if specified, overrides the default email invoice From: address.
614 my $template = scalar(@_) ? shift : '';
615 if ( scalar(@_) && $_[0] ) {
616 my $agentnums = ref($_[0]) ? shift : [ shift ];
617 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
623 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
625 my @invoicing_list = $self->cust_main->invoicing_list;
627 $self->email($template, $invoice_from)
628 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
630 $self->print($template)
631 if grep { $_ eq 'POST' } @invoicing_list; #postal
633 $self->fax($template)
634 if grep { $_ eq 'FAX' } @invoicing_list; #fax
640 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
644 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
646 INVOICE_FROM, if specified, overrides the default email invoice From: address.
652 my $template = scalar(@_) ? shift : '';
656 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
658 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
659 $self->cust_main->invoicing_list;
661 #better to notify this person than silence
662 @invoicing_list = ($invoice_from) unless @invoicing_list;
664 my $error = send_email(
665 $self->generate_email(
666 'from' => $invoice_from,
667 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
668 'template' => $template,
671 die "can't email invoice: $error\n" if $error;
672 #die "$error\n" if $error;
676 =item lpr_data [ TEMPLATENAME ]
678 Returns the postscript or plaintext for this invoice as an arrayref.
680 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
685 my( $self, $template) = @_;
686 $conf->exists('invoice_latex')
687 ? [ $self->print_ps('', $template) ]
688 : [ $self->print_text('', $template) ];
691 =item print [ TEMPLATENAME ]
695 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
701 my $template = scalar(@_) ? shift : '';
703 my $lpr = $conf->config('lpr');
706 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
708 $outerr = ": $outerr" if length($outerr);
709 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
714 =item fax [ TEMPLATENAME ]
718 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
724 my $template = scalar(@_) ? shift : '';
726 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
727 unless $conf->exists('invoice_latex');
729 my $dialstring = $self->cust_main->getfield('fax');
732 my $error = send_fax( 'docdata' => $self->lpr_data($template),
733 'dialstring' => $dialstring,
735 die $error if $error;
739 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
741 Like B<send>, but only sends the invoice if it is the newest open invoice for
751 grep { $_->owed > 0 }
752 qsearch('cust_bill', {
753 'custnum' => $self->custnum,
754 #'_date' => { op=>'>', value=>$self->_date },
755 'invnum' => { op=>'>', value=>$self->invnum },
762 =item send_csv OPTION => VALUE, ...
764 Sends invoice as a CSV data-file to a remote host with the specified protocol.
768 protocol - currently only "ftp"
774 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
775 and YYMMDDHHMMSS is a timestamp.
777 See L</print_csv> for a description of the output format.
782 my($self, %opt) = @_;
786 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
787 mkdir $spooldir, 0700 unless -d $spooldir;
789 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
790 my $file = "$spooldir/$tracctnum.csv";
792 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
794 open(CSV, ">$file") or die "can't open $file: $!";
802 if ( $opt{protocol} eq 'ftp' ) {
803 eval "use Net::FTP;";
805 $net = Net::FTP->new($opt{server}) or die @$;
807 die "unknown protocol: $opt{protocol}";
810 $net->login( $opt{username}, $opt{password} )
811 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
813 $net->binary or die "can't set binary mode";
815 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
817 $net->put($file) or die "can't put $file: $!";
827 Spools CSV invoice data.
833 =item format - 'default' or 'billco'
835 =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>).
837 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
839 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
846 my($self, %opt) = @_;
848 my $cust_main = $self->cust_main;
850 if ( $opt{'dest'} ) {
851 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
852 $cust_main->invoicing_list;
853 return 'N/A' unless $invoicing_list{$opt{'dest'}}
854 || ! keys %invoicing_list;
857 if ( $opt{'balanceover'} ) {
859 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
862 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
863 mkdir $spooldir, 0700 unless -d $spooldir;
865 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
869 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
870 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
873 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
875 open(CSV, ">>$file") or die "can't open $file: $!";
881 if ( lc($opt{'format'}) eq 'billco' ) {
888 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
891 open(CSV,">>$file") or die "can't open $file: $!";
905 =item print_csv OPTION => VALUE, ...
907 Returns CSV data for this invoice.
911 format - 'default' or 'billco'
913 Returns a list consisting of two scalars. The first is a single line of CSV
914 header information for this invoice. The second is one or more lines of CSV
915 detail information for this invoice.
917 If I<format> is not specified or "default", the fields of the CSV file are as
920 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
924 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
926 B<record_type> is C<cust_bill> for the initial header line only. The
927 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
928 fields are filled in.
930 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
931 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
934 =item invnum - invoice number
936 =item custnum - customer number
938 =item _date - invoice date
940 =item charged - total invoice amount
942 =item first - customer first name
944 =item last - customer first name
946 =item company - company name
948 =item address1 - address line 1
950 =item address2 - address line 1
960 =item pkg - line item description
962 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
964 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
966 =item sdate - start date for recurring fee
968 =item edate - end date for recurring fee
972 If I<format> is "billco", the fields of the header CSV file are as follows:
974 +-------------------------------------------------------------------+
975 | FORMAT HEADER FILE |
976 |-------------------------------------------------------------------|
977 | Field | Description | Name | Type | Width |
978 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
979 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
980 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
981 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
982 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
983 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
984 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
985 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
986 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
987 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
988 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
989 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
990 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
991 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
992 | 15 | Previous Balance | BALFWD | NUM* | 9 |
993 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
994 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
995 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
996 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
997 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
998 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
999 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1000 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1001 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1002 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1003 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1004 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1005 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1006 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1007 +-------+-------------------------------+------------+------+-------+
1009 If I<format> is "billco", the fields of the detail CSV file are as follows:
1011 FORMAT FOR DETAIL FILE
1013 Field | Description | Name | Type | Width
1014 1 | N/A-Leave Empty | RC | CHAR | 2
1015 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1016 3 | Account Number | TRACCTNUM | CHAR | 15
1017 4 | Invoice Number | TRINVOICE | CHAR | 15
1018 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1019 6 | Transaction Detail | DETAILS | CHAR | 100
1020 7 | Amount | AMT | NUM* | 9
1021 8 | Line Format Control** | LNCTRL | CHAR | 2
1022 9 | Grouping Code | GROUP | CHAR | 2
1023 10 | User Defined | ACCT CODE | CHAR | 15
1028 my($self, %opt) = @_;
1030 eval "use Text::CSV_XS";
1033 my $cust_main = $self->cust_main;
1035 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1037 if ( lc($opt{'format'}) eq 'billco' ) {
1040 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1043 if ( $conf->exists('invoice_default_terms')
1044 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1045 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1048 my( $previous_balance, @unused ) = $self->previous; #previous balance
1050 my $pmt_cr_applied = 0;
1051 $pmt_cr_applied += $_->{'amount'}
1052 foreach ( $self->_items_payments, $self->_items_credits ) ;
1054 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1057 '', # 1 | N/A-Leave Empty CHAR 2
1058 '', # 2 | N/A-Leave Empty CHAR 15
1059 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1060 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1061 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1062 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1063 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1064 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1065 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1066 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1067 '', # 10 | Ancillary Billing Information CHAR 30
1068 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1069 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1072 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1075 $duedate, # 14 | Bill Due Date CHAR 10
1077 $previous_balance, # 15 | Previous Balance NUM* 9
1078 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1079 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1080 $totaldue, # 18 | Total Amt Due NUM* 9
1081 $totaldue, # 19 | Total Amt Due NUM* 9
1082 '', # 20 | 30 Day Aging NUM* 9
1083 '', # 21 | 60 Day Aging NUM* 9
1084 '', # 22 | 90 Day Aging NUM* 9
1085 'N', # 23 | Y/N CHAR 1
1086 '', # 24 | Remittance automation CHAR 100
1087 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1088 $self->custnum, # 26 | Customer Reference Number CHAR 15
1089 '0', # 27 | Federal Tax*** NUM* 9
1090 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1091 '0', # 29 | Other Taxes & Fees*** NUM* 9
1100 time2str("%x", $self->_date),
1101 sprintf("%.2f", $self->charged),
1102 ( map { $cust_main->getfield($_) }
1103 qw( first last company address1 address2 city state zip country ) ),
1105 ) or die "can't create csv";
1108 my $header = $csv->string. "\n";
1111 if ( lc($opt{'format'}) eq 'billco' ) {
1114 foreach my $item ( $self->_items_pkg ) {
1117 '', # 1 | N/A-Leave Empty CHAR 2
1118 '', # 2 | N/A-Leave Empty CHAR 15
1119 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1120 $self->invnum, # 4 | Invoice Number CHAR 15
1121 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1122 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1123 $item->{'amount'}, # 7 | Amount NUM* 9
1124 '', # 8 | Line Format Control** CHAR 2
1125 '', # 9 | Grouping Code CHAR 2
1126 '', # 10 | User Defined CHAR 15
1129 $detail .= $csv->string. "\n";
1135 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1137 my($pkg, $setup, $recur, $sdate, $edate);
1138 if ( $cust_bill_pkg->pkgnum ) {
1140 ($pkg, $setup, $recur, $sdate, $edate) = (
1141 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1142 ( $cust_bill_pkg->setup != 0
1143 ? sprintf("%.2f", $cust_bill_pkg->setup )
1145 ( $cust_bill_pkg->recur != 0
1146 ? sprintf("%.2f", $cust_bill_pkg->recur )
1148 ( $cust_bill_pkg->sdate
1149 ? time2str("%x", $cust_bill_pkg->sdate)
1151 ($cust_bill_pkg->edate
1152 ?time2str("%x", $cust_bill_pkg->edate)
1156 } else { #pkgnum tax
1157 next unless $cust_bill_pkg->setup != 0;
1158 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1159 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1161 ($pkg, $setup, $recur, $sdate, $edate) =
1162 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1168 ( map { '' } (1..11) ),
1169 ($pkg, $setup, $recur, $sdate, $edate)
1170 ) or die "can't create csv";
1172 $detail .= $csv->string. "\n";
1178 ( $header, $detail );
1184 Pays this invoice with a compliemntary payment. If there is an error,
1185 returns the error, otherwise returns false.
1191 my $cust_pay = new FS::cust_pay ( {
1192 'invnum' => $self->invnum,
1193 'paid' => $self->owed,
1196 'payinfo' => $self->cust_main->payinfo,
1204 Attempts to pay this invoice with a credit card payment via a
1205 Business::OnlinePayment realtime gateway. See
1206 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1207 for supported processors.
1213 $self->realtime_bop( 'CC', @_ );
1218 Attempts to pay this invoice with an electronic check (ACH) payment via a
1219 Business::OnlinePayment realtime gateway. See
1220 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1221 for supported processors.
1227 $self->realtime_bop( 'ECHECK', @_ );
1232 Attempts to pay this invoice with phone bill (LEC) payment via a
1233 Business::OnlinePayment realtime gateway. See
1234 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1235 for supported processors.
1241 $self->realtime_bop( 'LEC', @_ );
1245 my( $self, $method ) = @_;
1247 my $cust_main = $self->cust_main;
1248 my $balance = $cust_main->balance;
1249 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1250 $amount = sprintf("%.2f", $amount);
1251 return "not run (balance $balance)" unless $amount > 0;
1253 my $description = 'Internet Services';
1254 if ( $conf->exists('business-onlinepayment-description') ) {
1255 my $dtempl = $conf->config('business-onlinepayment-description');
1257 my $agent_obj = $cust_main->agent
1258 or die "can't retreive agent for $cust_main (agentnum ".
1259 $cust_main->agentnum. ")";
1260 my $agent = $agent_obj->agent;
1261 my $pkgs = join(', ',
1262 map { $_->cust_pkg->part_pkg->pkg }
1263 grep { $_->pkgnum } $self->cust_bill_pkg
1265 $description = eval qq("$dtempl");
1268 $cust_main->realtime_bop($method, $amount,
1269 'description' => $description,
1270 'invnum' => $self->invnum,
1277 Adds a payment for this invoice to the pending credit card batch (see
1278 L<FS::cust_pay_batch>).
1284 my $cust_main = $self->cust_main;
1286 my $cust_pay_batch = new FS::cust_pay_batch ( {
1287 'invnum' => $self->getfield('invnum'),
1288 'custnum' => $cust_main->getfield('custnum'),
1289 'last' => $cust_main->getfield('last'),
1290 'first' => $cust_main->getfield('first'),
1291 'address1' => $cust_main->getfield('address1'),
1292 'address2' => $cust_main->getfield('address2'),
1293 'city' => $cust_main->getfield('city'),
1294 'state' => $cust_main->getfield('state'),
1295 'zip' => $cust_main->getfield('zip'),
1296 'country' => $cust_main->getfield('country'),
1297 'cardnum' => $cust_main->payinfo,
1298 'exp' => $cust_main->getfield('paydate'),
1299 'payname' => $cust_main->getfield('payname'),
1300 'amount' => $self->owed,
1302 my $error = $cust_pay_batch->insert;
1303 die $error if $error;
1308 sub _agent_template {
1310 $self->_agent_plandata('agent_templatename');
1313 sub _agent_invoice_from {
1315 $self->_agent_plandata('agent_invoice_from');
1318 sub _agent_plandata {
1319 my( $self, $option ) = @_;
1321 my $part_bill_event = qsearchs( 'part_bill_event',
1323 'payby' => $self->cust_main->payby,
1324 'plan' => 'send_agent',
1325 'plandata' => { 'op' => '~',
1326 'value' => "(^|\n)agentnum ".
1328 $self->cust_main->agentnum.
1334 'ORDER BY seconds LIMIT 1'
1337 return '' unless $part_bill_event;
1339 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1342 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1343 " plandata for $option";
1349 =item print_text [ TIME [ , TEMPLATE ] ]
1351 Returns an text invoice, as a list of lines.
1353 TIME an optional value used to control the printing of overdue messages. The
1354 default is now. It isn't the date of the invoice; that's the `_date' field.
1355 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1356 L<Time::Local> and L<Date::Parse> for conversion functions.
1360 #still some false laziness w/_items stuff (and send_csv)
1363 my( $self, $today, $template ) = @_;
1366 # my $invnum = $self->invnum;
1367 my $cust_main = $self->cust_main;
1368 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1369 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1371 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1372 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1373 #my $balance_due = $self->owed + $pr_total - $cr_total;
1374 my $balance_due = $self->owed + $pr_total;
1377 #my($description,$amount);
1381 foreach ( @pr_cust_bill ) {
1383 "Previous Balance, Invoice #". $_->invnum.
1384 " (". time2str("%x",$_->_date). ")",
1385 $money_char. sprintf("%10.2f",$_->owed)
1388 if (@pr_cust_bill) {
1389 push @buf,['','-----------'];
1390 push @buf,[ 'Total Previous Balance',
1391 $money_char. sprintf("%10.2f",$pr_total ) ];
1396 foreach my $cust_bill_pkg (
1397 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1398 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1401 my $desc = $cust_bill_pkg->desc;
1403 if ( $cust_bill_pkg->pkgnum > 0 ) {
1405 if ( $cust_bill_pkg->setup != 0 ) {
1406 my $description = $desc;
1407 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1408 push @buf, [ $description,
1409 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1411 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1412 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1415 if ( $cust_bill_pkg->recur != 0 ) {
1417 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1418 time2str("%x", $cust_bill_pkg->edate) . ")",
1419 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1422 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1423 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1424 $cust_bill_pkg->sdate );
1427 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1429 } else { #pkgnum tax or one-shot line item
1431 if ( $cust_bill_pkg->setup != 0 ) {
1433 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1435 if ( $cust_bill_pkg->recur != 0 ) {
1436 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1437 . time2str("%x", $cust_bill_pkg->edate). ")",
1438 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1446 push @buf,['','-----------'];
1447 push @buf,['Total New Charges',
1448 $money_char. sprintf("%10.2f",$self->charged) ];
1451 push @buf,['','-----------'];
1452 push @buf,['Total Charges',
1453 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1457 foreach ( $self->cust_credited ) {
1459 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1461 my $reason = substr($_->cust_credit->reason,0,32);
1462 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1463 $reason = " ($reason) " if $reason;
1465 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1467 $money_char. sprintf("%10.2f",$_->amount)
1470 #foreach ( @cr_cust_credit ) {
1472 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1473 # $money_char. sprintf("%10.2f",$_->credited)
1477 #get & print payments
1478 foreach ( $self->cust_bill_pay ) {
1480 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1483 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1484 $money_char. sprintf("%10.2f",$_->amount )
1489 my $balance_due_msg = $self->balance_due_msg;
1491 push @buf,['','-----------'];
1492 push @buf,[$balance_due_msg, $money_char.
1493 sprintf("%10.2f", $balance_due ) ];
1495 #create the template
1496 $template ||= $self->_agent_template;
1497 my $templatefile = 'invoice_template';
1498 $templatefile .= "_$template" if length($template);
1499 my @invoice_template = $conf->config($templatefile)
1500 or die "cannot load config file $templatefile";
1503 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1504 /invoice_lines\((\d*)\)/;
1505 $invoice_lines += $1 || scalar(@buf);
1508 die "no invoice_lines() functions in template?" unless $wasfunc;
1509 my $invoice_template = new Text::Template (
1511 SOURCE => [ map "$_\n", @invoice_template ],
1512 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1513 $invoice_template->compile()
1514 or die "can't compile template: $Text::Template::ERROR";
1516 #setup template variables
1517 package FS::cust_bill::_template; #!
1518 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1520 $invnum = $self->invnum;
1521 $date = $self->_date;
1523 $agent = $self->cust_main->agent->agent;
1525 if ( $FS::cust_bill::invoice_lines ) {
1527 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1529 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1534 #format address (variable for the template)
1536 @address = ( '', '', '', '', '', '' );
1537 package FS::cust_bill; #!
1538 $FS::cust_bill::_template::address[$l++] =
1539 $cust_main->payname.
1540 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1541 ? " (P.O. #". $cust_main->payinfo. ")"
1545 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1546 if $cust_main->company;
1547 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1548 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1549 if $cust_main->address2;
1550 $FS::cust_bill::_template::address[$l++] =
1551 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1553 my $countrydefault = $conf->config('countrydefault') || 'US';
1554 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1555 unless $cust_main->country eq $countrydefault;
1557 # #overdue? (variable for the template)
1558 # $FS::cust_bill::_template::overdue = (
1560 # && $today > $self->_date
1561 ## && $self->printed > 1
1562 # && $self->printed > 0
1565 #and subroutine for the template
1566 sub FS::cust_bill::_template::invoice_lines {
1567 my $lines = shift || scalar(@buf);
1569 scalar(@buf) ? shift @buf : [ '', '' ];
1575 $FS::cust_bill::_template::page = 1;
1579 push @collect, split("\n",
1580 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1582 $FS::cust_bill::_template::page++;
1585 map "$_\n", @collect;
1589 =item print_latex [ TIME [ , TEMPLATE ] ]
1591 Internal method - returns a filename of a filled-in LaTeX template for this
1592 invoice (Note: add ".tex" to get the actual filename).
1594 See print_ps and print_pdf for methods that return PostScript and PDF output.
1596 TIME an optional value used to control the printing of overdue messages. The
1597 default is now. It isn't the date of the invoice; that's the `_date' field.
1598 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1599 L<Time::Local> and L<Date::Parse> for conversion functions.
1603 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1606 my( $self, $today, $template ) = @_;
1608 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1611 my $cust_main = $self->cust_main;
1612 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1613 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1615 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1616 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1617 #my $balance_due = $self->owed + $pr_total - $cr_total;
1618 my $balance_due = $self->owed + $pr_total;
1620 #create the template
1621 $template ||= $self->_agent_template;
1622 my $templatefile = 'invoice_latex';
1623 my $suffix = length($template) ? "_$template" : '';
1624 $templatefile .= $suffix;
1625 my @invoice_template = map "$_\n", $conf->config($templatefile)
1626 or die "cannot load config file $templatefile";
1628 my($format, $text_template);
1629 if ( grep { /^%%Detail/ } @invoice_template ) {
1630 #change this to a die when the old code is removed
1631 warn "old-style invoice template $templatefile; ".
1632 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1635 $format = 'Text::Template';
1636 $text_template = new Text::Template(
1638 SOURCE => \@invoice_template,
1639 DELIMITERS => [ '[@--', '--@]' ],
1642 $text_template->compile()
1643 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1647 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1648 $returnaddress = join("\n",
1649 $conf->config_orbase('invoice_latexreturnaddress', $template)
1652 $returnaddress = '~';
1655 my %invoice_data = (
1656 'invnum' => $self->invnum,
1657 'date' => time2str('%b %o, %Y', $self->_date),
1658 'today' => time2str('%b %o, %Y', $today),
1659 'agent' => _latex_escape($cust_main->agent->agent),
1660 'payname' => _latex_escape($cust_main->payname),
1661 'company' => _latex_escape($cust_main->company),
1662 'address1' => _latex_escape($cust_main->address1),
1663 'address2' => _latex_escape($cust_main->address2),
1664 'city' => _latex_escape($cust_main->city),
1665 'state' => _latex_escape($cust_main->state),
1666 'zip' => _latex_escape($cust_main->zip),
1667 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1668 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1669 'returnaddress' => $returnaddress,
1671 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1672 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1673 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1676 my $countrydefault = $conf->config('countrydefault') || 'US';
1677 if ( $cust_main->country eq $countrydefault ) {
1678 $invoice_data{'country'} = '';
1680 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1683 $invoice_data{'notes'} =
1685 # #do variable substitutions in notes
1686 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1687 $conf->config_orbase('invoice_latexnotes', $template)
1689 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1692 $invoice_data{'footer'} =~ s/\n+$//;
1693 $invoice_data{'smallfooter'} =~ s/\n+$//;
1694 $invoice_data{'notes'} =~ s/\n+$//;
1696 $invoice_data{'po_line'} =
1697 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1698 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1702 if ( $format eq 'old' ) {
1705 my @total_item = ();
1706 while ( @invoice_template ) {
1707 my $line = shift @invoice_template;
1709 if ( $line =~ /^%%Detail\s*$/ ) {
1711 while ( ( my $line_item_line = shift @invoice_template )
1712 !~ /^%%EndDetail\s*$/ ) {
1713 push @line_item, $line_item_line;
1715 foreach my $line_item ( $self->_items ) {
1716 #foreach my $line_item ( $self->_items_pkg ) {
1717 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1718 $invoice_data{'description'} =
1719 _latex_escape($line_item->{'description'});
1720 if ( exists $line_item->{'ext_description'} ) {
1721 $invoice_data{'description'} .=
1722 "\\tabularnewline\n~~".
1723 join( "\\tabularnewline\n~~",
1724 map _latex_escape($_), @{$line_item->{'ext_description'}}
1727 $invoice_data{'amount'} = $line_item->{'amount'};
1728 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1730 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1733 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1735 while ( ( my $total_item_line = shift @invoice_template )
1736 !~ /^%%EndTotalDetails\s*$/ ) {
1737 push @total_item, $total_item_line;
1740 my @total_fill = ();
1743 foreach my $tax ( $self->_items_tax ) {
1744 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1745 $taxtotal += $tax->{'amount'};
1746 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1748 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1753 $invoice_data{'total_item'} = 'Sub-total';
1754 $invoice_data{'total_amount'} =
1755 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1756 unshift @total_fill,
1757 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1761 $invoice_data{'total_item'} = '\textbf{Total}';
1762 $invoice_data{'total_amount'} =
1763 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1765 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1768 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1771 foreach my $credit ( $self->_items_credits ) {
1772 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1774 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1776 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1781 foreach my $payment ( $self->_items_payments ) {
1782 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1784 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1786 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1790 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1791 $invoice_data{'total_amount'} =
1792 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1794 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1797 push @filled_in, @total_fill;
1800 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1801 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1802 push @filled_in, $line;
1813 } elsif ( $format eq 'Text::Template' ) {
1815 my @detail_items = ();
1816 my @total_items = ();
1818 $invoice_data{'detail_items'} = \@detail_items;
1819 $invoice_data{'total_items'} = \@total_items;
1821 foreach my $line_item ( $self->_items ) {
1823 ext_description => [],
1825 $detail->{'ref'} = $line_item->{'pkgnum'};
1826 $detail->{'quantity'} = 1;
1827 $detail->{'description'} = _latex_escape($line_item->{'description'});
1828 if ( exists $line_item->{'ext_description'} ) {
1829 @{$detail->{'ext_description'}} = map {
1831 } @{$line_item->{'ext_description'}};
1833 $detail->{'amount'} = $line_item->{'amount'};
1834 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1836 push @detail_items, $detail;
1841 foreach my $tax ( $self->_items_tax ) {
1843 $total->{'total_item'} = _latex_escape($tax->{'description'});
1844 $taxtotal += $tax->{'amount'};
1845 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1846 push @total_items, $total;
1851 $total->{'total_item'} = 'Sub-total';
1852 $total->{'total_amount'} =
1853 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1854 unshift @total_items, $total;
1859 $total->{'total_item'} = '\textbf{Total}';
1860 $total->{'total_amount'} =
1861 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1862 push @total_items, $total;
1865 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1868 foreach my $credit ( $self->_items_credits ) {
1870 $total->{'total_item'} = _latex_escape($credit->{'description'});
1872 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1873 push @total_items, $total;
1877 foreach my $payment ( $self->_items_payments ) {
1879 $total->{'total_item'} = _latex_escape($payment->{'description'});
1881 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1882 push @total_items, $total;
1887 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1888 $total->{'total_amount'} =
1889 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1890 push @total_items, $total;
1894 die "guru meditation #54";
1897 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1898 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1902 ) or die "can't open temp file: $!\n";
1903 if ( $format eq 'old' ) {
1904 print $fh join('', @filled_in );
1905 } elsif ( $format eq 'Text::Template' ) {
1906 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1908 die "guru meditation #32";
1912 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1917 =item print_ps [ TIME [ , TEMPLATE ] ]
1919 Returns an postscript invoice, as a scalar.
1921 TIME an optional value used to control the printing of overdue messages. The
1922 default is now. It isn't the date of the invoice; that's the `_date' field.
1923 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1924 L<Time::Local> and L<Date::Parse> for conversion functions.
1931 my $file = $self->print_latex(@_);
1933 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1936 my $sfile = shell_quote $file;
1938 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1939 or die "pslatex $file.tex failed; see $file.log for details?\n";
1940 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1941 or die "pslatex $file.tex failed; see $file.log for details?\n";
1943 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1944 or die "dvips failed";
1946 open(POSTSCRIPT, "<$file.ps")
1947 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1949 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1952 while (<POSTSCRIPT>) {
1962 =item print_pdf [ TIME [ , TEMPLATE ] ]
1964 Returns an PDF invoice, as a scalar.
1966 TIME an optional value used to control the printing of overdue messages. The
1967 default is now. It isn't the date of the invoice; that's the `_date' field.
1968 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1969 L<Time::Local> and L<Date::Parse> for conversion functions.
1976 my $file = $self->print_latex(@_);
1978 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1981 #system('pdflatex', "$file.tex");
1982 #system('pdflatex', "$file.tex");
1983 #! LaTeX Error: Unknown graphics extension: .eps.
1985 my $sfile = shell_quote $file;
1987 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1988 or die "pslatex $file.tex failed; see $file.log for details?\n";
1989 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1990 or die "pslatex $file.tex failed; see $file.log for details?\n";
1992 #system('dvipdf', "$file.dvi", "$file.pdf" );
1994 "dvips -q -t letter -f $sfile.dvi ".
1995 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1998 or die "dvips | gs failed: $!";
2000 open(PDF, "<$file.pdf")
2001 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2003 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2016 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2018 Returns an HTML invoice, as a scalar.
2020 TIME an optional value used to control the printing of overdue messages. The
2021 default is now. It isn't the date of the invoice; that's the `_date' field.
2022 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2023 L<Time::Local> and L<Date::Parse> for conversion functions.
2025 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2026 when emailing the invoice as part of a multipart/related MIME email.
2030 #some falze laziness w/print_text and print_latex (and send_csv)
2032 my( $self, $today, $template, $cid ) = @_;
2035 my $cust_main = $self->cust_main;
2036 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2037 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2039 $template ||= $self->_agent_template;
2040 my $templatefile = 'invoice_html';
2041 my $suffix = length($template) ? "_$template" : '';
2042 $templatefile .= $suffix;
2043 my @html_template = map "$_\n", $conf->config($templatefile)
2044 or die "cannot load config file $templatefile";
2046 my $html_template = new Text::Template(
2048 SOURCE => \@html_template,
2049 DELIMITERS => [ '<%=', '%>' ],
2052 $html_template->compile()
2053 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2055 my %invoice_data = (
2056 'invnum' => $self->invnum,
2057 'date' => time2str('%b %o, %Y', $self->_date),
2058 'today' => time2str('%b %o, %Y', $today),
2059 'agent' => encode_entities($cust_main->agent->agent),
2060 'payname' => encode_entities($cust_main->payname),
2061 'company' => encode_entities($cust_main->company),
2062 'address1' => encode_entities($cust_main->address1),
2063 'address2' => encode_entities($cust_main->address2),
2064 'city' => encode_entities($cust_main->city),
2065 'state' => encode_entities($cust_main->state),
2066 'zip' => encode_entities($cust_main->zip),
2067 'terms' => $conf->config('invoice_default_terms')
2068 || 'Payable upon receipt',
2070 'template' => $template,
2071 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2075 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2076 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2078 $invoice_data{'returnaddress'} =
2079 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2081 $invoice_data{'returnaddress'} =
2084 s/\\\\\*?\s*$/<BR>/;
2085 s/\\hyphenation\{[\w\s\-]+\}//;
2088 $conf->config_orbase( 'invoice_latexreturnaddress',
2094 my $countrydefault = $conf->config('countrydefault') || 'US';
2095 if ( $cust_main->country eq $countrydefault ) {
2096 $invoice_data{'country'} = '';
2098 $invoice_data{'country'} =
2099 encode_entities(code2country($cust_main->country));
2103 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2104 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2106 $invoice_data{'notes'} =
2107 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2109 $invoice_data{'notes'} =
2111 s/%%(.*)$/<!-- $1 -->/;
2112 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2113 s/\\begin\{enumerate\}/<ol>/;
2115 s/\\end\{enumerate\}/<\/ol>/;
2116 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2119 $conf->config_orbase('invoice_latexnotes', $template)
2123 # #do variable substitutions in notes
2124 # $invoice_data{'notes'} =
2126 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2127 # $conf->config_orbase('invoice_latexnotes', $suffix)
2131 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2132 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2134 $invoice_data{'footer'} =
2135 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2137 $invoice_data{'footer'} =
2138 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2139 $conf->config_orbase('invoice_latexfooter', $template)
2143 $invoice_data{'po_line'} =
2144 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2145 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2148 my $money_char = $conf->config('money_char') || '$';
2150 foreach my $line_item ( $self->_items ) {
2152 ext_description => [],
2154 $detail->{'ref'} = $line_item->{'pkgnum'};
2155 $detail->{'description'} = encode_entities($line_item->{'description'});
2156 if ( exists $line_item->{'ext_description'} ) {
2157 @{$detail->{'ext_description'}} = map {
2158 encode_entities($_);
2159 } @{$line_item->{'ext_description'}};
2161 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2162 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2164 push @{$invoice_data{'detail_items'}}, $detail;
2169 foreach my $tax ( $self->_items_tax ) {
2171 $total->{'total_item'} = encode_entities($tax->{'description'});
2172 $taxtotal += $tax->{'amount'};
2173 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2174 push @{$invoice_data{'total_items'}}, $total;
2179 $total->{'total_item'} = 'Sub-total';
2180 $total->{'total_amount'} =
2181 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2182 unshift @{$invoice_data{'total_items'}}, $total;
2185 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2188 $total->{'total_item'} = '<b>Total</b>';
2189 $total->{'total_amount'} =
2190 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2191 push @{$invoice_data{'total_items'}}, $total;
2194 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2197 foreach my $credit ( $self->_items_credits ) {
2199 $total->{'total_item'} = encode_entities($credit->{'description'});
2201 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2202 push @{$invoice_data{'total_items'}}, $total;
2206 foreach my $payment ( $self->_items_payments ) {
2208 $total->{'total_item'} = encode_entities($payment->{'description'});
2210 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2211 push @{$invoice_data{'total_items'}}, $total;
2216 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2217 $total->{'total_amount'} =
2218 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2219 push @{$invoice_data{'total_items'}}, $total;
2222 $html_template->fill_in( HASH => \%invoice_data);
2225 # quick subroutine for print_latex
2227 # There are ten characters that LaTeX treats as special characters, which
2228 # means that they do not simply typeset themselves:
2229 # # $ % & ~ _ ^ \ { }
2231 # TeX ignores blanks following an escaped character; if you want a blank (as
2232 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2236 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2237 $value =~ s/([<>])/\$$1\$/g;
2241 #utility methods for print_*
2243 sub balance_due_msg {
2245 my $msg = 'Balance Due';
2246 return $msg unless $conf->exists('invoice_default_terms');
2247 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2248 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2249 } elsif ( $conf->config('invoice_default_terms') ) {
2250 $msg .= ' - '. $conf->config('invoice_default_terms');
2257 my @display = scalar(@_)
2259 : qw( _items_previous _items_pkg );
2260 #: qw( _items_pkg );
2261 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2263 foreach my $display ( @display ) {
2264 push @b, $self->$display(@_);
2269 sub _items_previous {
2271 my $cust_main = $self->cust_main;
2272 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2274 foreach ( @pr_cust_bill ) {
2276 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2277 ' ('. time2str('%x',$_->_date). ')',
2278 #'pkgpart' => 'N/A',
2280 'amount' => sprintf("%.2f", $_->owed),
2286 # 'description' => 'Previous Balance',
2287 # #'pkgpart' => 'N/A',
2288 # 'pkgnum' => 'N/A',
2289 # 'amount' => sprintf("%10.2f", $pr_total ),
2290 # 'ext_description' => [ map {
2291 # "Invoice ". $_->invnum.
2292 # " (". time2str("%x",$_->_date). ") ".
2293 # sprintf("%10.2f", $_->owed)
2294 # } @pr_cust_bill ],
2301 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2302 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2307 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2308 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2311 sub _items_cust_bill_pkg {
2313 my $cust_bill_pkg = shift;
2316 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2318 my $desc = $cust_bill_pkg->desc;
2320 if ( $cust_bill_pkg->pkgnum > 0 ) {
2322 if ( $cust_bill_pkg->setup != 0 ) {
2323 my $description = $desc;
2324 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2325 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2326 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2328 description => $description,
2329 #pkgpart => $part_pkg->pkgpart,
2330 pkgnum => $cust_bill_pkg->pkgnum,
2331 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2332 ext_description => \@d,
2336 if ( $cust_bill_pkg->recur != 0 ) {
2338 description => "$desc (" .
2339 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2340 time2str('%x', $cust_bill_pkg->edate). ')',
2341 #pkgpart => $part_pkg->pkgpart,
2342 pkgnum => $cust_bill_pkg->pkgnum,
2343 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2345 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2346 $cust_bill_pkg->sdate),
2347 $cust_bill_pkg->details,
2352 } else { #pkgnum tax or one-shot line item (??)
2354 if ( $cust_bill_pkg->setup != 0 ) {
2356 'description' => $desc,
2357 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2360 if ( $cust_bill_pkg->recur != 0 ) {
2362 'description' => "$desc (".
2363 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2364 time2str("%x", $cust_bill_pkg->edate). ')',
2365 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2377 sub _items_credits {
2382 foreach ( $self->cust_credited ) {
2384 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2386 my $reason = $_->cust_credit->reason;
2387 #my $reason = substr($_->cust_credit->reason,0,32);
2388 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2389 $reason = " ($reason) " if $reason;
2391 #'description' => 'Credit ref\#'. $_->crednum.
2392 # " (". time2str("%x",$_->cust_credit->_date) .")".
2394 'description' => 'Credit applied '.
2395 time2str("%x",$_->cust_credit->_date). $reason,
2396 'amount' => sprintf("%.2f",$_->amount),
2399 #foreach ( @cr_cust_credit ) {
2401 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2402 # $money_char. sprintf("%10.2f",$_->credited)
2410 sub _items_payments {
2414 #get & print payments
2415 foreach ( $self->cust_bill_pay ) {
2417 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2420 'description' => "Payment received ".
2421 time2str("%x",$_->cust_pay->_date ),
2422 'amount' => sprintf("%.2f", $_->amount )
2441 sub process_reprint {
2442 process_re_X('print', @_);
2449 sub process_reemail {
2450 process_re_X('email', @_);
2458 process_re_X('fax', @_);
2461 use Storable qw(thaw);
2465 my( $method, $job ) = ( shift, shift );
2467 my $param = thaw(decode_base64(shift));
2468 warn Dumper($param) if $DEBUG;
2479 my($method, $job, %param ) = @_;
2480 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2482 #some false laziness w/search/cust_bill.html
2484 my $orderby = 'ORDER BY cust_bill._date';
2488 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2489 push @where, "cust_bill._date >= $1";
2491 if ( $param{'end'} =~ /^(\d+)$/ ) {
2492 push @where, "cust_bill._date < $1";
2494 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2495 push @where, "cust_main.agentnum = $1";
2499 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2500 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2501 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2502 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2504 push @where, "0 != $owed"
2507 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2510 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2512 my $addl_from = 'left join cust_main using ( custnum )';
2514 if ( $param{'newest_percust'} ) {
2515 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2516 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2517 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2520 my @cust_bill = qsearch( 'cust_bill',
2522 "$distinct cust_bill.*",
2528 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2529 foreach my $cust_bill ( @cust_bill ) {
2530 $cust_bill->$method();
2532 if ( $job ) { #progressbar foo
2534 if ( time - $min_sec > $last ) {
2535 my $error = $job->update_statustext(
2536 int( 100 * $num / scalar(@cust_bill) )
2538 die $error if $error;
2553 print_text formatting (and some logic :/) is in source, but needs to be
2554 slurped in from a file. Also number of lines ($=).
2558 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2559 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base