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
844 my($self, %opt) = @_;
846 my $cust_main = $self->cust_main;
848 if ( $opt{'dest'} ) {
849 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
850 $cust_main->invoicing_list;
851 return 'N/A' unless $invoicing_list{$opt{'dest'}}
852 || ! keys %invoicing_list;
855 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
856 mkdir $spooldir, 0700 unless -d $spooldir;
858 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
862 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
863 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
866 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
868 open(CSV, ">>$file") or die "can't open $file: $!";
874 if ( lc($opt{'format'}) eq 'billco' ) {
881 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
884 open(CSV,">>$file") or die "can't open $file: $!";
898 =item print_csv OPTION => VALUE, ...
900 Returns CSV data for this invoice.
904 format - 'default' or 'billco'
906 Returns a list consisting of two scalars. The first is a single line of CSV
907 header information for this invoice. The second is one or more lines of CSV
908 detail information for this invoice.
910 If I<format> is not specified or "default", the fields of the CSV file are as
913 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
917 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
919 B<record_type> is C<cust_bill> for the initial header line only. The
920 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
921 fields are filled in.
923 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
924 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
927 =item invnum - invoice number
929 =item custnum - customer number
931 =item _date - invoice date
933 =item charged - total invoice amount
935 =item first - customer first name
937 =item last - customer first name
939 =item company - company name
941 =item address1 - address line 1
943 =item address2 - address line 1
953 =item pkg - line item description
955 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
957 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
959 =item sdate - start date for recurring fee
961 =item edate - end date for recurring fee
965 If I<format> is "billco", the fields of the header CSV file are as follows:
967 +-------------------------------------------------------------------+
968 | FORMAT HEADER FILE |
969 |-------------------------------------------------------------------|
970 | Field | Description | Name | Type | Width |
971 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
972 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
973 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
974 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
975 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
976 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
977 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
978 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
979 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
980 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
981 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
982 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
983 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
984 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
985 | 15 | Previous Balance | BALFWD | NUM* | 9 |
986 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
987 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
988 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
989 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
990 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
991 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
992 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
993 | 23 | Y/N | AGESWITCH | CHAR | 1 |
994 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
995 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
996 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
997 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
998 | 28 | State Tax*** | STATETAX | NUM* | 9 |
999 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1000 +-------+-------------------------------+------------+------+-------+
1002 If I<format> is "billco", the fields of the detail CSV file are as follows:
1004 FORMAT FOR DETAIL FILE
1006 Field | Description | Name | Type | Width
1007 1 | N/A-Leave Empty | RC | CHAR | 2
1008 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1009 3 | Account Number | TRACCTNUM | CHAR | 15
1010 4 | Invoice Number | TRINVOICE | CHAR | 15
1011 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1012 6 | Transaction Detail | DETAILS | CHAR | 100
1013 7 | Amount | AMT | NUM* | 9
1014 8 | Line Format Control** | LNCTRL | CHAR | 2
1015 9 | Grouping Code | GROUP | CHAR | 2
1016 10 | User Defined | ACCT CODE | CHAR | 15
1021 my($self, %opt) = @_;
1023 eval "use Text::CSV_XS";
1026 my $cust_main = $self->cust_main;
1028 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1030 if ( lc($opt{'format'}) eq 'billco' ) {
1033 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1036 if ( $conf->exists('invoice_default_terms')
1037 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1038 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1041 my( $previous_balance, @unused ) = $self->previous; #previous balance
1043 my $pmt_cr_applied = 0;
1044 $pmt_cr_applied += $_->{'amount'}
1045 foreach ( $self->_items_payments, $self->_items_credits ) ;
1047 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1050 '', # 1 | N/A-Leave Empty CHAR 2
1051 '', # 2 | N/A-Leave Empty CHAR 15
1052 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1053 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1054 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1055 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1056 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1057 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1058 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1059 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1060 '', # 10 | Ancillary Billing Information CHAR 30
1061 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1062 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1065 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1068 $duedate, # 14 | Bill Due Date CHAR 10
1070 $previous_balance, # 15 | Previous Balance NUM* 9
1071 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1072 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1073 $totaldue, # 18 | Total Amt Due NUM* 9
1074 $totaldue, # 19 | Total Amt Due NUM* 9
1075 '', # 20 | 30 Day Aging NUM* 9
1076 '', # 21 | 60 Day Aging NUM* 9
1077 '', # 22 | 90 Day Aging NUM* 9
1078 'N', # 23 | Y/N CHAR 1
1079 '', # 24 | Remittance automation CHAR 100
1080 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1081 $self->custnum, # 26 | Customer Reference Number CHAR 15
1082 '0', # 27 | Federal Tax*** NUM* 9
1083 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1084 '0', # 29 | Other Taxes & Fees*** NUM* 9
1093 time2str("%x", $self->_date),
1094 sprintf("%.2f", $self->charged),
1095 ( map { $cust_main->getfield($_) }
1096 qw( first last company address1 address2 city state zip country ) ),
1098 ) or die "can't create csv";
1101 my $header = $csv->string. "\n";
1104 if ( lc($opt{'format'}) eq 'billco' ) {
1107 foreach my $item ( $self->_items_pkg ) {
1110 '', # 1 | N/A-Leave Empty CHAR 2
1111 '', # 2 | N/A-Leave Empty CHAR 15
1112 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1113 $self->invnum, # 4 | Invoice Number CHAR 15
1114 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1115 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1116 $item->{'amount'}, # 7 | Amount NUM* 9
1117 '', # 8 | Line Format Control** CHAR 2
1118 '', # 9 | Grouping Code CHAR 2
1119 '', # 10 | User Defined CHAR 15
1122 $detail .= $csv->string. "\n";
1128 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1130 my($pkg, $setup, $recur, $sdate, $edate);
1131 if ( $cust_bill_pkg->pkgnum ) {
1133 ($pkg, $setup, $recur, $sdate, $edate) = (
1134 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1135 ( $cust_bill_pkg->setup != 0
1136 ? sprintf("%.2f", $cust_bill_pkg->setup )
1138 ( $cust_bill_pkg->recur != 0
1139 ? sprintf("%.2f", $cust_bill_pkg->recur )
1141 ( $cust_bill_pkg->sdate
1142 ? time2str("%x", $cust_bill_pkg->sdate)
1144 ($cust_bill_pkg->edate
1145 ?time2str("%x", $cust_bill_pkg->edate)
1149 } else { #pkgnum tax
1150 next unless $cust_bill_pkg->setup != 0;
1151 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1152 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1154 ($pkg, $setup, $recur, $sdate, $edate) =
1155 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1161 ( map { '' } (1..11) ),
1162 ($pkg, $setup, $recur, $sdate, $edate)
1163 ) or die "can't create csv";
1165 $detail .= $csv->string. "\n";
1171 ( $header, $detail );
1177 Pays this invoice with a compliemntary payment. If there is an error,
1178 returns the error, otherwise returns false.
1184 my $cust_pay = new FS::cust_pay ( {
1185 'invnum' => $self->invnum,
1186 'paid' => $self->owed,
1189 'payinfo' => $self->cust_main->payinfo,
1197 Attempts to pay this invoice with a credit card payment via a
1198 Business::OnlinePayment realtime gateway. See
1199 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1200 for supported processors.
1206 $self->realtime_bop( 'CC', @_ );
1211 Attempts to pay this invoice with an electronic check (ACH) payment via a
1212 Business::OnlinePayment realtime gateway. See
1213 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1214 for supported processors.
1220 $self->realtime_bop( 'ECHECK', @_ );
1225 Attempts to pay this invoice with phone bill (LEC) payment via a
1226 Business::OnlinePayment realtime gateway. See
1227 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1228 for supported processors.
1234 $self->realtime_bop( 'LEC', @_ );
1238 my( $self, $method ) = @_;
1240 my $cust_main = $self->cust_main;
1241 my $balance = $cust_main->balance;
1242 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1243 $amount = sprintf("%.2f", $amount);
1244 return "not run (balance $balance)" unless $amount > 0;
1246 my $description = 'Internet Services';
1247 if ( $conf->exists('business-onlinepayment-description') ) {
1248 my $dtempl = $conf->config('business-onlinepayment-description');
1250 my $agent_obj = $cust_main->agent
1251 or die "can't retreive agent for $cust_main (agentnum ".
1252 $cust_main->agentnum. ")";
1253 my $agent = $agent_obj->agent;
1254 my $pkgs = join(', ',
1255 map { $_->cust_pkg->part_pkg->pkg }
1256 grep { $_->pkgnum } $self->cust_bill_pkg
1258 $description = eval qq("$dtempl");
1261 $cust_main->realtime_bop($method, $amount,
1262 'description' => $description,
1263 'invnum' => $self->invnum,
1270 Adds a payment for this invoice to the pending credit card batch (see
1271 L<FS::cust_pay_batch>).
1277 my $cust_main = $self->cust_main;
1279 my $cust_pay_batch = new FS::cust_pay_batch ( {
1280 'invnum' => $self->getfield('invnum'),
1281 'custnum' => $cust_main->getfield('custnum'),
1282 'last' => $cust_main->getfield('last'),
1283 'first' => $cust_main->getfield('first'),
1284 'address1' => $cust_main->getfield('address1'),
1285 'address2' => $cust_main->getfield('address2'),
1286 'city' => $cust_main->getfield('city'),
1287 'state' => $cust_main->getfield('state'),
1288 'zip' => $cust_main->getfield('zip'),
1289 'country' => $cust_main->getfield('country'),
1290 'cardnum' => $cust_main->payinfo,
1291 'exp' => $cust_main->getfield('paydate'),
1292 'payname' => $cust_main->getfield('payname'),
1293 'amount' => $self->owed,
1295 my $error = $cust_pay_batch->insert;
1296 die $error if $error;
1301 sub _agent_template {
1303 $self->_agent_plandata('agent_templatename');
1306 sub _agent_invoice_from {
1308 $self->_agent_plandata('agent_invoice_from');
1311 sub _agent_plandata {
1312 my( $self, $option ) = @_;
1314 my $part_bill_event = qsearchs( 'part_bill_event',
1316 'payby' => $self->cust_main->payby,
1317 'plan' => 'send_agent',
1318 'plandata' => { 'op' => '~',
1319 'value' => "(^|\n)agentnum ".
1321 $self->cust_main->agentnum.
1327 'ORDER BY seconds LIMIT 1'
1330 return '' unless $part_bill_event;
1332 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1335 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1336 " plandata for $option";
1342 =item print_text [ TIME [ , TEMPLATE ] ]
1344 Returns an text invoice, as a list of lines.
1346 TIME an optional value used to control the printing of overdue messages. The
1347 default is now. It isn't the date of the invoice; that's the `_date' field.
1348 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1349 L<Time::Local> and L<Date::Parse> for conversion functions.
1353 #still some false laziness w/_items stuff (and send_csv)
1356 my( $self, $today, $template ) = @_;
1359 # my $invnum = $self->invnum;
1360 my $cust_main = $self->cust_main;
1361 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1362 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1364 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1365 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1366 #my $balance_due = $self->owed + $pr_total - $cr_total;
1367 my $balance_due = $self->owed + $pr_total;
1370 #my($description,$amount);
1374 foreach ( @pr_cust_bill ) {
1376 "Previous Balance, Invoice #". $_->invnum.
1377 " (". time2str("%x",$_->_date). ")",
1378 $money_char. sprintf("%10.2f",$_->owed)
1381 if (@pr_cust_bill) {
1382 push @buf,['','-----------'];
1383 push @buf,[ 'Total Previous Balance',
1384 $money_char. sprintf("%10.2f",$pr_total ) ];
1389 foreach my $cust_bill_pkg (
1390 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1391 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1394 my $desc = $cust_bill_pkg->desc;
1396 if ( $cust_bill_pkg->pkgnum > 0 ) {
1398 if ( $cust_bill_pkg->setup != 0 ) {
1399 my $description = $desc;
1400 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1401 push @buf, [ $description,
1402 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1404 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1405 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1408 if ( $cust_bill_pkg->recur != 0 ) {
1410 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1411 time2str("%x", $cust_bill_pkg->edate) . ")",
1412 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1415 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1416 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1417 $cust_bill_pkg->sdate );
1420 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1422 } else { #pkgnum tax or one-shot line item
1424 if ( $cust_bill_pkg->setup != 0 ) {
1426 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1428 if ( $cust_bill_pkg->recur != 0 ) {
1429 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1430 . time2str("%x", $cust_bill_pkg->edate). ")",
1431 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1439 push @buf,['','-----------'];
1440 push @buf,['Total New Charges',
1441 $money_char. sprintf("%10.2f",$self->charged) ];
1444 push @buf,['','-----------'];
1445 push @buf,['Total Charges',
1446 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1450 foreach ( $self->cust_credited ) {
1452 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1454 my $reason = substr($_->cust_credit->reason,0,32);
1455 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1456 $reason = " ($reason) " if $reason;
1458 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1460 $money_char. sprintf("%10.2f",$_->amount)
1463 #foreach ( @cr_cust_credit ) {
1465 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1466 # $money_char. sprintf("%10.2f",$_->credited)
1470 #get & print payments
1471 foreach ( $self->cust_bill_pay ) {
1473 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1476 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1477 $money_char. sprintf("%10.2f",$_->amount )
1482 my $balance_due_msg = $self->balance_due_msg;
1484 push @buf,['','-----------'];
1485 push @buf,[$balance_due_msg, $money_char.
1486 sprintf("%10.2f", $balance_due ) ];
1488 #create the template
1489 $template ||= $self->_agent_template;
1490 my $templatefile = 'invoice_template';
1491 $templatefile .= "_$template" if length($template);
1492 my @invoice_template = $conf->config($templatefile)
1493 or die "cannot load config file $templatefile";
1496 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1497 /invoice_lines\((\d*)\)/;
1498 $invoice_lines += $1 || scalar(@buf);
1501 die "no invoice_lines() functions in template?" unless $wasfunc;
1502 my $invoice_template = new Text::Template (
1504 SOURCE => [ map "$_\n", @invoice_template ],
1505 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1506 $invoice_template->compile()
1507 or die "can't compile template: $Text::Template::ERROR";
1509 #setup template variables
1510 package FS::cust_bill::_template; #!
1511 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1513 $invnum = $self->invnum;
1514 $date = $self->_date;
1516 $agent = $self->cust_main->agent->agent;
1518 if ( $FS::cust_bill::invoice_lines ) {
1520 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1522 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1527 #format address (variable for the template)
1529 @address = ( '', '', '', '', '', '' );
1530 package FS::cust_bill; #!
1531 $FS::cust_bill::_template::address[$l++] =
1532 $cust_main->payname.
1533 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1534 ? " (P.O. #". $cust_main->payinfo. ")"
1538 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1539 if $cust_main->company;
1540 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1541 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1542 if $cust_main->address2;
1543 $FS::cust_bill::_template::address[$l++] =
1544 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1546 my $countrydefault = $conf->config('countrydefault') || 'US';
1547 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1548 unless $cust_main->country eq $countrydefault;
1550 # #overdue? (variable for the template)
1551 # $FS::cust_bill::_template::overdue = (
1553 # && $today > $self->_date
1554 ## && $self->printed > 1
1555 # && $self->printed > 0
1558 #and subroutine for the template
1559 sub FS::cust_bill::_template::invoice_lines {
1560 my $lines = shift || scalar(@buf);
1562 scalar(@buf) ? shift @buf : [ '', '' ];
1568 $FS::cust_bill::_template::page = 1;
1572 push @collect, split("\n",
1573 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1575 $FS::cust_bill::_template::page++;
1578 map "$_\n", @collect;
1582 =item print_latex [ TIME [ , TEMPLATE ] ]
1584 Internal method - returns a filename of a filled-in LaTeX template for this
1585 invoice (Note: add ".tex" to get the actual filename).
1587 See print_ps and print_pdf for methods that return PostScript and PDF output.
1589 TIME an optional value used to control the printing of overdue messages. The
1590 default is now. It isn't the date of the invoice; that's the `_date' field.
1591 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1592 L<Time::Local> and L<Date::Parse> for conversion functions.
1596 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1599 my( $self, $today, $template ) = @_;
1601 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1604 my $cust_main = $self->cust_main;
1605 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1606 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1608 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1609 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1610 #my $balance_due = $self->owed + $pr_total - $cr_total;
1611 my $balance_due = $self->owed + $pr_total;
1613 #create the template
1614 $template ||= $self->_agent_template;
1615 my $templatefile = 'invoice_latex';
1616 my $suffix = length($template) ? "_$template" : '';
1617 $templatefile .= $suffix;
1618 my @invoice_template = map "$_\n", $conf->config($templatefile)
1619 or die "cannot load config file $templatefile";
1621 my($format, $text_template);
1622 if ( grep { /^%%Detail/ } @invoice_template ) {
1623 #change this to a die when the old code is removed
1624 warn "old-style invoice template $templatefile; ".
1625 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1628 $format = 'Text::Template';
1629 $text_template = new Text::Template(
1631 SOURCE => \@invoice_template,
1632 DELIMITERS => [ '[@--', '--@]' ],
1635 $text_template->compile()
1636 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1640 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1641 $returnaddress = join("\n",
1642 $conf->config_orbase('invoice_latexreturnaddress', $template)
1645 $returnaddress = '~';
1648 my %invoice_data = (
1649 'invnum' => $self->invnum,
1650 'date' => time2str('%b %o, %Y', $self->_date),
1651 'today' => time2str('%b %o, %Y', $today),
1652 'agent' => _latex_escape($cust_main->agent->agent),
1653 'payname' => _latex_escape($cust_main->payname),
1654 'company' => _latex_escape($cust_main->company),
1655 'address1' => _latex_escape($cust_main->address1),
1656 'address2' => _latex_escape($cust_main->address2),
1657 'city' => _latex_escape($cust_main->city),
1658 'state' => _latex_escape($cust_main->state),
1659 'zip' => _latex_escape($cust_main->zip),
1660 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1661 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1662 'returnaddress' => $returnaddress,
1664 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1665 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1666 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1669 my $countrydefault = $conf->config('countrydefault') || 'US';
1670 if ( $cust_main->country eq $countrydefault ) {
1671 $invoice_data{'country'} = '';
1673 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1676 $invoice_data{'notes'} =
1678 # #do variable substitutions in notes
1679 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1680 $conf->config_orbase('invoice_latexnotes', $template)
1682 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1685 $invoice_data{'footer'} =~ s/\n+$//;
1686 $invoice_data{'smallfooter'} =~ s/\n+$//;
1687 $invoice_data{'notes'} =~ s/\n+$//;
1689 $invoice_data{'po_line'} =
1690 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1691 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1695 if ( $format eq 'old' ) {
1698 my @total_item = ();
1699 while ( @invoice_template ) {
1700 my $line = shift @invoice_template;
1702 if ( $line =~ /^%%Detail\s*$/ ) {
1704 while ( ( my $line_item_line = shift @invoice_template )
1705 !~ /^%%EndDetail\s*$/ ) {
1706 push @line_item, $line_item_line;
1708 foreach my $line_item ( $self->_items ) {
1709 #foreach my $line_item ( $self->_items_pkg ) {
1710 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1711 $invoice_data{'description'} =
1712 _latex_escape($line_item->{'description'});
1713 if ( exists $line_item->{'ext_description'} ) {
1714 $invoice_data{'description'} .=
1715 "\\tabularnewline\n~~".
1716 join( "\\tabularnewline\n~~",
1717 map _latex_escape($_), @{$line_item->{'ext_description'}}
1720 $invoice_data{'amount'} = $line_item->{'amount'};
1721 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1723 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1726 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1728 while ( ( my $total_item_line = shift @invoice_template )
1729 !~ /^%%EndTotalDetails\s*$/ ) {
1730 push @total_item, $total_item_line;
1733 my @total_fill = ();
1736 foreach my $tax ( $self->_items_tax ) {
1737 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1738 $taxtotal += $tax->{'amount'};
1739 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1741 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1746 $invoice_data{'total_item'} = 'Sub-total';
1747 $invoice_data{'total_amount'} =
1748 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1749 unshift @total_fill,
1750 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1754 $invoice_data{'total_item'} = '\textbf{Total}';
1755 $invoice_data{'total_amount'} =
1756 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1758 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1761 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1764 foreach my $credit ( $self->_items_credits ) {
1765 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1767 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1769 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1774 foreach my $payment ( $self->_items_payments ) {
1775 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1777 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1779 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1783 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1784 $invoice_data{'total_amount'} =
1785 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1787 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1790 push @filled_in, @total_fill;
1793 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1794 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1795 push @filled_in, $line;
1806 } elsif ( $format eq 'Text::Template' ) {
1808 my @detail_items = ();
1809 my @total_items = ();
1811 $invoice_data{'detail_items'} = \@detail_items;
1812 $invoice_data{'total_items'} = \@total_items;
1814 foreach my $line_item ( $self->_items ) {
1816 ext_description => [],
1818 $detail->{'ref'} = $line_item->{'pkgnum'};
1819 $detail->{'quantity'} = 1;
1820 $detail->{'description'} = _latex_escape($line_item->{'description'});
1821 if ( exists $line_item->{'ext_description'} ) {
1822 @{$detail->{'ext_description'}} = map {
1824 } @{$line_item->{'ext_description'}};
1826 $detail->{'amount'} = $line_item->{'amount'};
1827 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1829 push @detail_items, $detail;
1834 foreach my $tax ( $self->_items_tax ) {
1836 $total->{'total_item'} = _latex_escape($tax->{'description'});
1837 $taxtotal += $tax->{'amount'};
1838 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1839 push @total_items, $total;
1844 $total->{'total_item'} = 'Sub-total';
1845 $total->{'total_amount'} =
1846 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1847 unshift @total_items, $total;
1852 $total->{'total_item'} = '\textbf{Total}';
1853 $total->{'total_amount'} =
1854 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1855 push @total_items, $total;
1858 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1861 foreach my $credit ( $self->_items_credits ) {
1863 $total->{'total_item'} = _latex_escape($credit->{'description'});
1865 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1866 push @total_items, $total;
1870 foreach my $payment ( $self->_items_payments ) {
1872 $total->{'total_item'} = _latex_escape($payment->{'description'});
1874 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1875 push @total_items, $total;
1880 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1881 $total->{'total_amount'} =
1882 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1883 push @total_items, $total;
1887 die "guru meditation #54";
1890 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1891 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1895 ) or die "can't open temp file: $!\n";
1896 if ( $format eq 'old' ) {
1897 print $fh join('', @filled_in );
1898 } elsif ( $format eq 'Text::Template' ) {
1899 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1901 die "guru meditation #32";
1905 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1910 =item print_ps [ TIME [ , TEMPLATE ] ]
1912 Returns an postscript invoice, as a scalar.
1914 TIME an optional value used to control the printing of overdue messages. The
1915 default is now. It isn't the date of the invoice; that's the `_date' field.
1916 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1917 L<Time::Local> and L<Date::Parse> for conversion functions.
1924 my $file = $self->print_latex(@_);
1926 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1929 my $sfile = shell_quote $file;
1931 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1932 or die "pslatex $file.tex failed; see $file.log for details?\n";
1933 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1934 or die "pslatex $file.tex failed; see $file.log for details?\n";
1936 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1937 or die "dvips failed";
1939 open(POSTSCRIPT, "<$file.ps")
1940 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1942 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1945 while (<POSTSCRIPT>) {
1955 =item print_pdf [ TIME [ , TEMPLATE ] ]
1957 Returns an PDF invoice, as a scalar.
1959 TIME an optional value used to control the printing of overdue messages. The
1960 default is now. It isn't the date of the invoice; that's the `_date' field.
1961 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1962 L<Time::Local> and L<Date::Parse> for conversion functions.
1969 my $file = $self->print_latex(@_);
1971 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1974 #system('pdflatex', "$file.tex");
1975 #system('pdflatex', "$file.tex");
1976 #! LaTeX Error: Unknown graphics extension: .eps.
1978 my $sfile = shell_quote $file;
1980 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1981 or die "pslatex $file.tex failed; see $file.log for details?\n";
1982 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1983 or die "pslatex $file.tex failed; see $file.log for details?\n";
1985 #system('dvipdf', "$file.dvi", "$file.pdf" );
1987 "dvips -q -t letter -f $sfile.dvi ".
1988 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1991 or die "dvips | gs failed: $!";
1993 open(PDF, "<$file.pdf")
1994 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1996 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2009 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2011 Returns an HTML invoice, as a scalar.
2013 TIME an optional value used to control the printing of overdue messages. The
2014 default is now. It isn't the date of the invoice; that's the `_date' field.
2015 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2016 L<Time::Local> and L<Date::Parse> for conversion functions.
2018 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2019 when emailing the invoice as part of a multipart/related MIME email.
2023 #some falze laziness w/print_text and print_latex (and send_csv)
2025 my( $self, $today, $template, $cid ) = @_;
2028 my $cust_main = $self->cust_main;
2029 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2030 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2032 $template ||= $self->_agent_template;
2033 my $templatefile = 'invoice_html';
2034 my $suffix = length($template) ? "_$template" : '';
2035 $templatefile .= $suffix;
2036 my @html_template = map "$_\n", $conf->config($templatefile)
2037 or die "cannot load config file $templatefile";
2039 my $html_template = new Text::Template(
2041 SOURCE => \@html_template,
2042 DELIMITERS => [ '<%=', '%>' ],
2045 $html_template->compile()
2046 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2048 my %invoice_data = (
2049 'invnum' => $self->invnum,
2050 'date' => time2str('%b %o, %Y', $self->_date),
2051 'today' => time2str('%b %o, %Y', $today),
2052 'agent' => encode_entities($cust_main->agent->agent),
2053 'payname' => encode_entities($cust_main->payname),
2054 'company' => encode_entities($cust_main->company),
2055 'address1' => encode_entities($cust_main->address1),
2056 'address2' => encode_entities($cust_main->address2),
2057 'city' => encode_entities($cust_main->city),
2058 'state' => encode_entities($cust_main->state),
2059 'zip' => encode_entities($cust_main->zip),
2060 'terms' => $conf->config('invoice_default_terms')
2061 || 'Payable upon receipt',
2063 'template' => $template,
2064 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2068 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2069 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2071 $invoice_data{'returnaddress'} =
2072 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2074 $invoice_data{'returnaddress'} =
2077 s/\\\\\*?\s*$/<BR>/;
2078 s/\\hyphenation\{[\w\s\-]+\}//;
2081 $conf->config_orbase( 'invoice_latexreturnaddress',
2087 my $countrydefault = $conf->config('countrydefault') || 'US';
2088 if ( $cust_main->country eq $countrydefault ) {
2089 $invoice_data{'country'} = '';
2091 $invoice_data{'country'} =
2092 encode_entities(code2country($cust_main->country));
2096 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2097 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2099 $invoice_data{'notes'} =
2100 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2102 $invoice_data{'notes'} =
2104 s/%%(.*)$/<!-- $1 -->/;
2105 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2106 s/\\begin\{enumerate\}/<ol>/;
2108 s/\\end\{enumerate\}/<\/ol>/;
2109 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2112 $conf->config_orbase('invoice_latexnotes', $template)
2116 # #do variable substitutions in notes
2117 # $invoice_data{'notes'} =
2119 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2120 # $conf->config_orbase('invoice_latexnotes', $suffix)
2124 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2125 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2127 $invoice_data{'footer'} =
2128 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2130 $invoice_data{'footer'} =
2131 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2132 $conf->config_orbase('invoice_latexfooter', $template)
2136 $invoice_data{'po_line'} =
2137 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2138 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2141 my $money_char = $conf->config('money_char') || '$';
2143 foreach my $line_item ( $self->_items ) {
2145 ext_description => [],
2147 $detail->{'ref'} = $line_item->{'pkgnum'};
2148 $detail->{'description'} = encode_entities($line_item->{'description'});
2149 if ( exists $line_item->{'ext_description'} ) {
2150 @{$detail->{'ext_description'}} = map {
2151 encode_entities($_);
2152 } @{$line_item->{'ext_description'}};
2154 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2155 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2157 push @{$invoice_data{'detail_items'}}, $detail;
2162 foreach my $tax ( $self->_items_tax ) {
2164 $total->{'total_item'} = encode_entities($tax->{'description'});
2165 $taxtotal += $tax->{'amount'};
2166 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2167 push @{$invoice_data{'total_items'}}, $total;
2172 $total->{'total_item'} = 'Sub-total';
2173 $total->{'total_amount'} =
2174 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2175 unshift @{$invoice_data{'total_items'}}, $total;
2178 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2181 $total->{'total_item'} = '<b>Total</b>';
2182 $total->{'total_amount'} =
2183 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2184 push @{$invoice_data{'total_items'}}, $total;
2187 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2190 foreach my $credit ( $self->_items_credits ) {
2192 $total->{'total_item'} = encode_entities($credit->{'description'});
2194 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2195 push @{$invoice_data{'total_items'}}, $total;
2199 foreach my $payment ( $self->_items_payments ) {
2201 $total->{'total_item'} = encode_entities($payment->{'description'});
2203 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2204 push @{$invoice_data{'total_items'}}, $total;
2209 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2210 $total->{'total_amount'} =
2211 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2212 push @{$invoice_data{'total_items'}}, $total;
2215 $html_template->fill_in( HASH => \%invoice_data);
2218 # quick subroutine for print_latex
2220 # There are ten characters that LaTeX treats as special characters, which
2221 # means that they do not simply typeset themselves:
2222 # # $ % & ~ _ ^ \ { }
2224 # TeX ignores blanks following an escaped character; if you want a blank (as
2225 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2229 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2230 $value =~ s/([<>])/\$$1\$/g;
2234 #utility methods for print_*
2236 sub balance_due_msg {
2238 my $msg = 'Balance Due';
2239 return $msg unless $conf->exists('invoice_default_terms');
2240 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2241 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2242 } elsif ( $conf->config('invoice_default_terms') ) {
2243 $msg .= ' - '. $conf->config('invoice_default_terms');
2250 my @display = scalar(@_)
2252 : qw( _items_previous _items_pkg );
2253 #: qw( _items_pkg );
2254 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2256 foreach my $display ( @display ) {
2257 push @b, $self->$display(@_);
2262 sub _items_previous {
2264 my $cust_main = $self->cust_main;
2265 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2267 foreach ( @pr_cust_bill ) {
2269 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2270 ' ('. time2str('%x',$_->_date). ')',
2271 #'pkgpart' => 'N/A',
2273 'amount' => sprintf("%.2f", $_->owed),
2279 # 'description' => 'Previous Balance',
2280 # #'pkgpart' => 'N/A',
2281 # 'pkgnum' => 'N/A',
2282 # 'amount' => sprintf("%10.2f", $pr_total ),
2283 # 'ext_description' => [ map {
2284 # "Invoice ". $_->invnum.
2285 # " (". time2str("%x",$_->_date). ") ".
2286 # sprintf("%10.2f", $_->owed)
2287 # } @pr_cust_bill ],
2294 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2295 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2300 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2301 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2304 sub _items_cust_bill_pkg {
2306 my $cust_bill_pkg = shift;
2309 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2311 my $desc = $cust_bill_pkg->desc;
2313 if ( $cust_bill_pkg->pkgnum > 0 ) {
2315 if ( $cust_bill_pkg->setup != 0 ) {
2316 my $description = $desc;
2317 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2318 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2319 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2321 description => $description,
2322 #pkgpart => $part_pkg->pkgpart,
2323 pkgnum => $cust_bill_pkg->pkgnum,
2324 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2325 ext_description => \@d,
2329 if ( $cust_bill_pkg->recur != 0 ) {
2331 description => "$desc (" .
2332 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2333 time2str('%x', $cust_bill_pkg->edate). ')',
2334 #pkgpart => $part_pkg->pkgpart,
2335 pkgnum => $cust_bill_pkg->pkgnum,
2336 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2338 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2339 $cust_bill_pkg->sdate),
2340 $cust_bill_pkg->details,
2345 } else { #pkgnum tax or one-shot line item (??)
2347 if ( $cust_bill_pkg->setup != 0 ) {
2349 'description' => $desc,
2350 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2353 if ( $cust_bill_pkg->recur != 0 ) {
2355 'description' => "$desc (".
2356 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2357 time2str("%x", $cust_bill_pkg->edate). ')',
2358 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2370 sub _items_credits {
2375 foreach ( $self->cust_credited ) {
2377 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2379 my $reason = $_->cust_credit->reason;
2380 #my $reason = substr($_->cust_credit->reason,0,32);
2381 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2382 $reason = " ($reason) " if $reason;
2384 #'description' => 'Credit ref\#'. $_->crednum.
2385 # " (". time2str("%x",$_->cust_credit->_date) .")".
2387 'description' => 'Credit applied '.
2388 time2str("%x",$_->cust_credit->_date). $reason,
2389 'amount' => sprintf("%.2f",$_->amount),
2392 #foreach ( @cr_cust_credit ) {
2394 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2395 # $money_char. sprintf("%10.2f",$_->credited)
2403 sub _items_payments {
2407 #get & print payments
2408 foreach ( $self->cust_bill_pay ) {
2410 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2413 'description' => "Payment received ".
2414 time2str("%x",$_->cust_pay->_date ),
2415 'amount' => sprintf("%.2f", $_->amount )
2434 sub process_reprint {
2435 process_re_X('print', @_);
2442 sub process_reemail {
2443 process_re_X('email', @_);
2451 process_re_X('fax', @_);
2454 use Storable qw(thaw);
2458 my( $method, $job ) = ( shift, shift );
2460 my $param = thaw(decode_base64(shift));
2461 warn Dumper($param) if $DEBUG;
2472 my($method, $job, %param ) = @_;
2473 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2475 #some false laziness w/search/cust_bill.html
2477 my $orderby = 'ORDER BY cust_bill._date';
2481 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2482 push @where, "cust_bill._date >= $1";
2484 if ( $param{'end'} =~ /^(\d+)$/ ) {
2485 push @where, "cust_bill._date < $1";
2487 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2488 push @where, "cust_main.agentnum = $1";
2492 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2493 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2494 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2495 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2497 push @where, "0 != $owed"
2500 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2503 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2505 my $addl_from = 'left join cust_main using ( custnum )';
2507 if ( $param{'newest_percust'} ) {
2508 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2509 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2510 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2513 my @cust_bill = qsearch( 'cust_bill',
2515 "$distinct cust_bill.*",
2521 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2522 foreach my $cust_bill ( @cust_bill ) {
2523 $cust_bill->$method();
2525 if ( $job ) { #progressbar foo
2527 if ( time - $min_sec > $last ) {
2528 my $error = $job->update_statustext(
2529 int( 100 * $num / scalar(@cust_bill) )
2531 die $error if $error;
2546 print_text formatting (and some logic :/) is in source, but needs to be
2547 slurped in from a file. Also number of lines ($=).
2551 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2552 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base