4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax );
16 use FS::Record qw( qsearch qsearchs );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
27 use FS::cust_bill_pay;
28 use FS::part_bill_event;
30 @ISA = qw( FS::cust_main_Mixin FS::Record );
34 #ask FS::UID to run this stuff for us later
35 FS::UID->install_callback( sub {
37 $money_char = $conf->config('money_char') || '$';
42 FS::cust_bill - Object methods for cust_bill records
48 $record = new FS::cust_bill \%hash;
49 $record = new FS::cust_bill { 'column' => 'value' };
51 $error = $record->insert;
53 $error = $new_record->replace($old_record);
55 $error = $record->delete;
57 $error = $record->check;
59 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
61 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
63 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
65 @cust_pay_objects = $cust_bill->cust_pay;
67 $tax_amount = $record->tax;
69 @lines = $cust_bill->print_text;
70 @lines = $cust_bill->print_text $time;
74 An FS::cust_bill object represents an invoice; a declaration that a customer
75 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
76 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
77 following fields are currently supported:
81 =item invnum - primary key (assigned automatically for new invoices)
83 =item custnum - customer (see L<FS::cust_main>)
85 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
86 L<Time::Local> and L<Date::Parse> for conversion functions.
88 =item charged - amount of this invoice
90 =item printed - deprecated
92 =item closed - books closed flag, empty or `Y'
102 Creates a new invoice. To add the invoice to the database, see L<"insert">.
103 Invoices are normally created by calling the bill method of a customer object
104 (see L<FS::cust_main>).
108 sub table { 'cust_bill'; }
110 sub cust_linked { $_[0]->cust_main_custnum; }
111 sub cust_unlinked_msg {
113 "WARNING: can't find cust_main.custnum ". $self->custnum.
114 ' (cust_bill.invnum '. $self->invnum. ')';
119 Adds this invoice to the database ("Posts" the invoice). If there is an error,
120 returns the error, otherwise returns false.
124 Currently unimplemented. I don't remove invoices because there would then be
125 no record you ever posted this invoice (which is bad, no?)
131 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
132 $self->SUPER::delete(@_);
135 =item replace OLD_RECORD
137 Replaces the OLD_RECORD with this one in the database. If there is an error,
138 returns the error, otherwise returns false.
140 Only printed may be changed. printed is normally updated by calling the
141 collect method of a customer object (see L<FS::cust_main>).
146 my( $new, $old ) = ( shift, shift );
147 return "Can't change custnum!" unless $old->custnum == $new->custnum;
148 #return "Can't change _date!" unless $old->_date eq $new->_date;
149 return "Can't change _date!" unless $old->_date == $new->_date;
150 return "Can't change charged!" unless $old->charged == $new->charged;
152 $new->SUPER::replace($old);
157 Checks all fields to make sure this is a valid invoice. If there is an error,
158 returns the error, otherwise returns false. Called by the insert and replace
167 $self->ut_numbern('invnum')
168 || $self->ut_number('custnum')
169 || $self->ut_numbern('_date')
170 || $self->ut_money('charged')
171 || $self->ut_numbern('printed')
172 || $self->ut_enum('closed', [ '', 'Y' ])
174 return $error if $error;
176 return "Unknown customer"
177 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
179 $self->_date(time) unless $self->_date;
181 $self->printed(0) if $self->printed eq '';
188 Returns a list consisting of the total previous balance for this customer,
189 followed by the previous outstanding invoices (as FS::cust_bill objects also).
196 my @cust_bill = sort { $a->_date <=> $b->_date }
197 grep { $_->owed != 0 && $_->_date < $self->_date }
198 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
200 foreach ( @cust_bill ) { $total += $_->owed; }
206 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
212 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
215 =item cust_bill_event
217 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
222 sub cust_bill_event {
224 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
230 Returns the customer (see L<FS::cust_main>) for this invoice.
236 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
239 =item cust_suspend_if_balance_over AMOUNT
241 Suspends the customer associated with this invoice if the total amount owed on
242 this invoice and all older invoices is greater than the specified amount.
244 Returns a list: an empty list on success or a list of errors.
248 sub cust_suspend_if_balance_over {
249 my( $self, $amount ) = ( shift, shift );
250 my $cust_main = $self->cust_main;
251 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
260 Depreciated. See the cust_credited method.
262 #Returns a list consisting of the total previous credited (see
263 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
264 #outstanding credits (FS::cust_credit objects).
270 croak "FS::cust_bill->cust_credit depreciated; see ".
271 "FS::cust_bill->cust_credit_bill";
274 #my @cust_credit = sort { $a->_date <=> $b->_date }
275 # grep { $_->credited != 0 && $_->_date < $self->_date }
276 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
278 #foreach (@cust_credit) { $total += $_->credited; }
279 #$total, @cust_credit;
284 Depreciated. See the cust_bill_pay method.
286 #Returns all payments (see L<FS::cust_pay>) for this invoice.
292 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
294 #sort { $a->_date <=> $b->_date }
295 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
301 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
307 sort { $a->_date <=> $b->_date }
308 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
313 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
319 sort { $a->_date <=> $b->_date }
320 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
326 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
333 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
335 foreach (@taxlines) { $total += $_->setup; }
341 Returns the amount owed (still outstanding) on this invoice, which is charged
342 minus all payment applications (see L<FS::cust_bill_pay>) and credit
343 applications (see L<FS::cust_credit_bill>).
349 my $balance = $self->charged;
350 $balance -= $_->amount foreach ( $self->cust_bill_pay );
351 $balance -= $_->amount foreach ( $self->cust_credited );
352 $balance = sprintf( "%.2f", $balance);
353 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
358 =item generate_email PARAMHASH
360 PARAMHASH can contain the following:
364 =item from => sender address, required
366 =item tempate => alternate template name, optional
368 =item print_text => text attachment arrayref, optional
370 =item subject => email subject, optional
374 Returns an argument list to be passed to L<FS::Misc::send_email>.
385 my $me = '[FS::cust_bill::generate_email]';
388 'from' => $args{'from'},
389 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
392 if (ref($args{'to'} eq 'ARRAY')) {
393 $return{'to'} = $args{'to'};
395 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
396 $self->cust_main->invoicing_list
400 if ( $conf->exists('invoice_html') ) {
402 warn "$me creating HTML/text multipart message"
405 $return{'nobody'} = 1;
407 my $alternative = build MIME::Entity
408 'Type' => 'multipart/alternative',
409 'Encoding' => '7bit',
410 'Disposition' => 'inline'
414 if ( $conf->exists('invoice_email_pdf')
415 and scalar($conf->config('invoice_email_pdf_note')) ) {
417 warn "$me using 'invoice_email_pdf_note' in multipart message"
419 $data = [ map { $_ . "\n" }
420 $conf->config('invoice_email_pdf_note')
425 warn "$me not using 'invoice_email_pdf_note' in multipart message"
427 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
428 $data = $args{'print_text'};
430 $data = [ $self->print_text('', $args{'template'}) ];
435 $alternative->attach(
436 'Type' => 'text/plain',
437 #'Encoding' => 'quoted-printable',
438 'Encoding' => '7bit',
440 'Disposition' => 'inline',
443 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
444 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
446 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
448 if ( defined($args{'_template'}) && length($args{'_template'})
449 && -e "$path/logo_". $args{'_template'}. ".png"
452 $file = "$path/logo_". $args{'_template'}. ".png";
454 $file = "$path/logo.png";
457 my $image = build MIME::Entity
458 'Type' => 'image/png',
459 'Encoding' => 'base64',
461 'Filename' => 'logo.png',
462 'Content-ID' => "<$content_id>",
465 $alternative->attach(
466 'Type' => 'text/html',
467 'Encoding' => 'quoted-printable',
468 'Data' => [ '<html>',
471 ' '. encode_entities($return{'subject'}),
474 ' <body bgcolor="#e8e8e8">',
475 $self->print_html('', $args{'template'}, $content_id),
479 'Disposition' => 'inline',
480 #'Filename' => 'invoice.pdf',
483 if ( $conf->exists('invoice_email_pdf') ) {
488 # multipart/alternative
494 my $related = build MIME::Entity 'Type' => 'multipart/related',
495 'Encoding' => '7bit';
497 #false laziness w/Misc::send_email
498 $related->head->replace('Content-type',
500 '; boundary="'. $related->head->multipart_boundary. '"'.
501 '; type=multipart/alternative'
504 $related->add_part($alternative);
506 $related->add_part($image);
508 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
510 $return{'mimeparts'} = [ $related, $pdf ];
514 #no other attachment:
516 # multipart/alternative
521 $return{'content-type'} = 'multipart/related';
522 $return{'mimeparts'} = [ $alternative, $image ];
523 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
524 #$return{'disposition'} = 'inline';
530 if ( $conf->exists('invoice_email_pdf') ) {
531 warn "$me creating PDF attachment"
534 #mime parts arguments a la MIME::Entity->build().
535 $return{'mimeparts'} = [
536 { $self->mimebuild_pdf('', $args{'template'}) }
540 if ( $conf->exists('invoice_email_pdf')
541 and scalar($conf->config('invoice_email_pdf_note')) ) {
543 warn "$me using 'invoice_email_pdf_note'"
545 $return{'body'} = [ map { $_ . "\n" }
546 $conf->config('invoice_email_pdf_note')
551 warn "$me not using 'invoice_email_pdf_note'"
553 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
554 $return{'body'} = $args{'print_text'};
556 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
569 Returns a list suitable for passing to MIME::Entity->build(), representing
570 this invoice as PDF attachment.
577 'Type' => 'application/pdf',
578 'Encoding' => 'base64',
579 'Data' => [ $self->print_pdf(@_) ],
580 'Disposition' => 'attachment',
581 'Filename' => 'invoice.pdf',
585 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
587 Sends this invoice to the destinations configured for this customer: sends
588 email, prints and/or faxes. See L<FS::cust_main_invoice>.
590 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
592 AGENTNUM, if specified, means that this invoice will only be sent for customers
593 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
594 single agent) or an arrayref of agentnums.
596 INVOICE_FROM, if specified, overrides the default email invoice From: address.
602 my $template = scalar(@_) ? shift : '';
603 if ( scalar(@_) && $_[0] ) {
604 my $agentnums = ref($_[0]) ? shift : [ shift ];
605 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
611 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
613 my @invoicing_list = $self->cust_main->invoicing_list;
615 $self->email($template, $invoice_from)
616 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
618 $self->print($template)
619 if grep { $_ eq 'POST' } @invoicing_list; #postal
621 $self->fax($template)
622 if grep { $_ eq 'FAX' } @invoicing_list; #fax
628 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
632 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
634 INVOICE_FROM, if specified, overrides the default email invoice From: address.
640 my $template = scalar(@_) ? shift : '';
644 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
646 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
647 $self->cust_main->invoicing_list;
649 #better to notify this person than silence
650 @invoicing_list = ($invoice_from) unless @invoicing_list;
652 my $error = send_email(
653 $self->generate_email(
654 'from' => $invoice_from,
655 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
656 'template' => $template,
659 die "can't email invoice: $error\n" if $error;
660 #die "$error\n" if $error;
664 =item lpr_data [ TEMPLATENAME ]
666 Returns the postscript or plaintext for this invoice as an arrayref.
668 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
673 my( $self, $template) = @_;
674 $conf->exists('invoice_latex')
675 ? [ $self->print_ps('', $template) ]
676 : [ $self->print_text('', $template) ];
679 =item print [ TEMPLATENAME ]
683 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
689 my $template = scalar(@_) ? shift : '';
691 my $lpr = $conf->config('lpr');
694 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
696 $outerr = ": $outerr" if length($outerr);
697 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
702 =item fax [ TEMPLATENAME ]
706 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
712 my $template = scalar(@_) ? shift : '';
714 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
715 unless $conf->exists('invoice_latex');
717 my $dialstring = $self->cust_main->getfield('fax');
720 my $error = send_fax( 'docdata' => $self->lpr_data($template),
721 'dialstring' => $dialstring,
723 die $error if $error;
727 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
729 Like B<send>, but only sends the invoice if it is the newest open invoice for
739 grep { $_->owed > 0 }
740 qsearch('cust_bill', {
741 'custnum' => $self->custnum,
742 #'_date' => { op=>'>', value=>$self->_date },
743 'invnum' => { op=>'>', value=>$self->invnum },
750 =item send_csv OPTION => VALUE, ...
752 Sends invoice as a CSV data-file to a remote host with the specified protocol.
756 protocol - currently only "ftp"
762 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
763 and YYMMDDHHMMSS is a timestamp.
765 See L</print_csv> for a description of the output format.
770 my($self, %opt) = @_;
774 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
775 mkdir $spooldir, 0700 unless -d $spooldir;
777 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
778 my $file = "$spooldir/$tracctnum.csv";
780 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
782 open(CSV, ">$file") or die "can't open $file: $!";
790 if ( $opt{protocol} eq 'ftp' ) {
791 eval "use Net::FTP;";
793 $net = Net::FTP->new($opt{server}) or die @$;
795 die "unknown protocol: $opt{protocol}";
798 $net->login( $opt{username}, $opt{password} )
799 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
801 $net->binary or die "can't set binary mode";
803 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
805 $net->put($file) or die "can't put $file: $!";
815 Spools CSV invoice data.
821 =item format - 'default' or 'billco'
823 =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>).
825 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
832 my($self, %opt) = @_;
834 my $cust_main = $self->cust_main;
836 if ( $opt{'dest'} ) {
837 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
838 $cust_main->invoicing_list;
839 return 'N/A' unless $invoicing_list{$opt{'dest'}}
840 || ! keys %invoicing_list;
843 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
844 mkdir $spooldir, 0700 unless -d $spooldir;
846 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
850 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
851 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
854 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
856 open(CSV, ">>$file") or die "can't open $file: $!";
862 if ( lc($opt{'format'}) eq 'billco' ) {
869 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
872 open(CSV,">>$file") or die "can't open $file: $!";
886 =item print_csv OPTION => VALUE, ...
888 Returns CSV data for this invoice.
892 format - 'default' or 'billco'
894 Returns a list consisting of two scalars. The first is a single line of CSV
895 header information for this invoice. The second is one or more lines of CSV
896 detail information for this invoice.
898 If I<format> is not specified or "default", the fields of the CSV file are as
901 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
905 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
907 B<record_type> is C<cust_bill> for the initial header line only. The
908 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
909 fields are filled in.
911 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
912 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
915 =item invnum - invoice number
917 =item custnum - customer number
919 =item _date - invoice date
921 =item charged - total invoice amount
923 =item first - customer first name
925 =item last - customer first name
927 =item company - company name
929 =item address1 - address line 1
931 =item address2 - address line 1
941 =item pkg - line item description
943 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
945 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
947 =item sdate - start date for recurring fee
949 =item edate - end date for recurring fee
953 If I<format> is "billco", the fields of the header CSV file are as follows:
955 +-------------------------------------------------------------------+
956 | FORMAT HEADER FILE |
957 |-------------------------------------------------------------------|
958 | Field | Description | Name | Type | Width |
959 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
960 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
961 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
962 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
963 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
964 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
965 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
966 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
967 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
968 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
969 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
970 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
971 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
972 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
973 | 15 | Previous Balance | BALFWD | NUM* | 9 |
974 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
975 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
976 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
977 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
978 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
979 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
980 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
981 | 23 | Y/N | AGESWITCH | CHAR | 1 |
982 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
983 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
984 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
985 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
986 | 28 | State Tax*** | STATETAX | NUM* | 9 |
987 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
988 +-------+-------------------------------+------------+------+-------+
990 If I<format> is "billco", the fields of the detail CSV file are as follows:
992 FORMAT FOR DETAIL FILE
994 Field | Description | Name | Type | Width
995 1 | N/A-Leave Empty | RC | CHAR | 2
996 2 | N/A-Leave Empty | CUSTID | CHAR | 15
997 3 | Account Number | TRACCTNUM | CHAR | 15
998 4 | Invoice Number | TRINVOICE | CHAR | 15
999 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1000 6 | Transaction Detail | DETAILS | CHAR | 100
1001 7 | Amount | AMT | NUM* | 9
1002 8 | Line Format Control** | LNCTRL | CHAR | 2
1003 9 | Grouping Code | GROUP | CHAR | 2
1004 10 | User Defined | ACCT CODE | CHAR | 15
1009 my($self, %opt) = @_;
1011 eval "use Text::CSV_XS";
1014 my $cust_main = $self->cust_main;
1016 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1018 if ( lc($opt{'format'}) eq 'billco' ) {
1021 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1024 if ( $conf->exists('invoice_default_terms')
1025 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
1026 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
1029 my( $previous_balance, @unused ) = $self->previous; #previous balance
1031 my $pmt_cr_applied = 0;
1032 $pmt_cr_applied += $_->{'amount'}
1033 foreach ( $self->_items_payments, $self->_items_credits ) ;
1035 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1038 '', # 1 | N/A-Leave Empty CHAR 2
1039 '', # 2 | N/A-Leave Empty CHAR 15
1040 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1041 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1042 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1043 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1044 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1045 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1046 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1047 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1048 '', # 10 | Ancillary Billing Information CHAR 30
1049 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1050 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1053 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1056 $duedate, # 14 | Bill Due Date CHAR 10
1058 $previous_balance, # 15 | Previous Balance NUM* 9
1059 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1060 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1061 $totaldue, # 18 | Total Amt Due NUM* 9
1062 $totaldue, # 19 | Total Amt Due NUM* 9
1063 '', # 20 | 30 Day Aging NUM* 9
1064 '', # 21 | 60 Day Aging NUM* 9
1065 '', # 22 | 90 Day Aging NUM* 9
1066 'N', # 23 | Y/N CHAR 1
1067 '', # 24 | Remittance automation CHAR 100
1068 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1069 $self->custnum, # 26 | Customer Reference Number CHAR 15
1070 '0', # 27 | Federal Tax*** NUM* 9
1071 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1072 '0', # 29 | Other Taxes & Fees*** NUM* 9
1081 time2str("%x", $self->_date),
1082 sprintf("%.2f", $self->charged),
1083 ( map { $cust_main->getfield($_) }
1084 qw( first last company address1 address2 city state zip country ) ),
1086 ) or die "can't create csv";
1089 my $header = $csv->string. "\n";
1092 if ( lc($opt{'format'}) eq 'billco' ) {
1095 foreach my $item ( $self->_items_pkg ) {
1098 '', # 1 | N/A-Leave Empty CHAR 2
1099 '', # 2 | N/A-Leave Empty CHAR 15
1100 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1101 $self->invnum, # 4 | Invoice Number CHAR 15
1102 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1103 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1104 $item->{'amount'}, # 7 | Amount NUM* 9
1105 '', # 8 | Line Format Control** CHAR 2
1106 '', # 9 | Grouping Code CHAR 2
1107 '', # 10 | User Defined CHAR 15
1110 $detail .= $csv->string. "\n";
1116 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1118 my($pkg, $setup, $recur, $sdate, $edate);
1119 if ( $cust_bill_pkg->pkgnum ) {
1121 ($pkg, $setup, $recur, $sdate, $edate) = (
1122 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1123 ( $cust_bill_pkg->setup != 0
1124 ? sprintf("%.2f", $cust_bill_pkg->setup )
1126 ( $cust_bill_pkg->recur != 0
1127 ? sprintf("%.2f", $cust_bill_pkg->recur )
1129 ( $cust_bill_pkg->sdate
1130 ? time2str("%x", $cust_bill_pkg->sdate)
1132 ($cust_bill_pkg->edate
1133 ?time2str("%x", $cust_bill_pkg->edate)
1137 } else { #pkgnum tax
1138 next unless $cust_bill_pkg->setup != 0;
1139 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1140 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1142 ($pkg, $setup, $recur, $sdate, $edate) =
1143 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1149 ( map { '' } (1..11) ),
1150 ($pkg, $setup, $recur, $sdate, $edate)
1151 ) or die "can't create csv";
1153 $detail .= $csv->string. "\n";
1159 ( $header, $detail );
1165 Pays this invoice with a compliemntary payment. If there is an error,
1166 returns the error, otherwise returns false.
1172 my $cust_pay = new FS::cust_pay ( {
1173 'invnum' => $self->invnum,
1174 'paid' => $self->owed,
1177 'payinfo' => $self->cust_main->payinfo,
1185 Attempts to pay this invoice with a credit card payment via a
1186 Business::OnlinePayment realtime gateway. See
1187 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1188 for supported processors.
1194 $self->realtime_bop( 'CC', @_ );
1199 Attempts to pay this invoice with an electronic check (ACH) payment via a
1200 Business::OnlinePayment realtime gateway. See
1201 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1202 for supported processors.
1208 $self->realtime_bop( 'ECHECK', @_ );
1213 Attempts to pay this invoice with phone bill (LEC) payment via a
1214 Business::OnlinePayment realtime gateway. See
1215 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1216 for supported processors.
1222 $self->realtime_bop( 'LEC', @_ );
1226 my( $self, $method ) = @_;
1228 my $cust_main = $self->cust_main;
1229 my $balance = $cust_main->balance;
1230 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1231 $amount = sprintf("%.2f", $amount);
1232 return "not run (balance $balance)" unless $amount > 0;
1234 my $description = 'Internet Services';
1235 if ( $conf->exists('business-onlinepayment-description') ) {
1236 my $dtempl = $conf->config('business-onlinepayment-description');
1238 my $agent_obj = $cust_main->agent
1239 or die "can't retreive agent for $cust_main (agentnum ".
1240 $cust_main->agentnum. ")";
1241 my $agent = $agent_obj->agent;
1242 my $pkgs = join(', ',
1243 map { $_->cust_pkg->part_pkg->pkg }
1244 grep { $_->pkgnum } $self->cust_bill_pkg
1246 $description = eval qq("$dtempl");
1249 $cust_main->realtime_bop($method, $amount,
1250 'description' => $description,
1251 'invnum' => $self->invnum,
1258 Adds a payment for this invoice to the pending credit card batch (see
1259 L<FS::cust_pay_batch>).
1265 my $cust_main = $self->cust_main;
1267 my $cust_pay_batch = new FS::cust_pay_batch ( {
1268 'invnum' => $self->getfield('invnum'),
1269 'custnum' => $cust_main->getfield('custnum'),
1270 'last' => $cust_main->getfield('last'),
1271 'first' => $cust_main->getfield('first'),
1272 'address1' => $cust_main->getfield('address1'),
1273 'address2' => $cust_main->getfield('address2'),
1274 'city' => $cust_main->getfield('city'),
1275 'state' => $cust_main->getfield('state'),
1276 'zip' => $cust_main->getfield('zip'),
1277 'country' => $cust_main->getfield('country'),
1278 'cardnum' => $cust_main->payinfo,
1279 'exp' => $cust_main->getfield('paydate'),
1280 'payname' => $cust_main->getfield('payname'),
1281 'amount' => $self->owed,
1283 my $error = $cust_pay_batch->insert;
1284 die $error if $error;
1289 sub _agent_template {
1291 $self->_agent_plandata('agent_templatename');
1294 sub _agent_invoice_from {
1296 $self->_agent_plandata('agent_invoice_from');
1299 sub _agent_plandata {
1300 my( $self, $option ) = @_;
1302 my $part_bill_event = qsearchs( 'part_bill_event',
1304 'payby' => $self->cust_main->payby,
1305 'plan' => 'send_agent',
1306 'plandata' => { 'op' => '~',
1307 'value' => "(^|\n)agentnum ".
1309 $self->cust_main->agentnum.
1315 'ORDER BY seconds LIMIT 1'
1318 return '' unless $part_bill_event;
1320 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1323 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1324 " plandata for $option";
1330 =item print_text [ TIME [ , TEMPLATE ] ]
1332 Returns an text invoice, as a list of lines.
1334 TIME an optional value used to control the printing of overdue messages. The
1335 default is now. It isn't the date of the invoice; that's the `_date' field.
1336 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1337 L<Time::Local> and L<Date::Parse> for conversion functions.
1341 #still some false laziness w/_items stuff (and send_csv)
1344 my( $self, $today, $template ) = @_;
1347 # my $invnum = $self->invnum;
1348 my $cust_main = $self->cust_main;
1349 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1350 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1352 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1353 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1354 #my $balance_due = $self->owed + $pr_total - $cr_total;
1355 my $balance_due = $self->owed + $pr_total;
1358 #my($description,$amount);
1362 foreach ( @pr_cust_bill ) {
1364 "Previous Balance, Invoice #". $_->invnum.
1365 " (". time2str("%x",$_->_date). ")",
1366 $money_char. sprintf("%10.2f",$_->owed)
1369 if (@pr_cust_bill) {
1370 push @buf,['','-----------'];
1371 push @buf,[ 'Total Previous Balance',
1372 $money_char. sprintf("%10.2f",$pr_total ) ];
1377 foreach my $cust_bill_pkg (
1378 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1379 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1382 my $desc = $cust_bill_pkg->desc;
1384 if ( $cust_bill_pkg->pkgnum > 0 ) {
1386 if ( $cust_bill_pkg->setup != 0 ) {
1387 my $description = $desc;
1388 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1389 push @buf, [ $description,
1390 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1392 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1393 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1396 if ( $cust_bill_pkg->recur != 0 ) {
1398 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1399 time2str("%x", $cust_bill_pkg->edate) . ")",
1400 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1403 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1404 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1405 $cust_bill_pkg->sdate );
1408 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1410 } else { #pkgnum tax or one-shot line item
1412 if ( $cust_bill_pkg->setup != 0 ) {
1414 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1416 if ( $cust_bill_pkg->recur != 0 ) {
1417 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1418 . time2str("%x", $cust_bill_pkg->edate). ")",
1419 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1427 push @buf,['','-----------'];
1428 push @buf,['Total New Charges',
1429 $money_char. sprintf("%10.2f",$self->charged) ];
1432 push @buf,['','-----------'];
1433 push @buf,['Total Charges',
1434 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1438 foreach ( $self->cust_credited ) {
1440 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1442 my $reason = substr($_->cust_credit->reason,0,32);
1443 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1444 $reason = " ($reason) " if $reason;
1446 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1448 $money_char. sprintf("%10.2f",$_->amount)
1451 #foreach ( @cr_cust_credit ) {
1453 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1454 # $money_char. sprintf("%10.2f",$_->credited)
1458 #get & print payments
1459 foreach ( $self->cust_bill_pay ) {
1461 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1464 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1465 $money_char. sprintf("%10.2f",$_->amount )
1470 my $balance_due_msg = $self->balance_due_msg;
1472 push @buf,['','-----------'];
1473 push @buf,[$balance_due_msg, $money_char.
1474 sprintf("%10.2f", $balance_due ) ];
1476 #create the template
1477 $template ||= $self->_agent_template;
1478 my $templatefile = 'invoice_template';
1479 $templatefile .= "_$template" if length($template);
1480 my @invoice_template = $conf->config($templatefile)
1481 or die "cannot load config file $templatefile";
1484 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1485 /invoice_lines\((\d*)\)/;
1486 $invoice_lines += $1 || scalar(@buf);
1489 die "no invoice_lines() functions in template?" unless $wasfunc;
1490 my $invoice_template = new Text::Template (
1492 SOURCE => [ map "$_\n", @invoice_template ],
1493 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1494 $invoice_template->compile()
1495 or die "can't compile template: $Text::Template::ERROR";
1497 #setup template variables
1498 package FS::cust_bill::_template; #!
1499 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1501 $invnum = $self->invnum;
1502 $date = $self->_date;
1504 $agent = $self->cust_main->agent->agent;
1506 if ( $FS::cust_bill::invoice_lines ) {
1508 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1510 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1515 #format address (variable for the template)
1517 @address = ( '', '', '', '', '', '' );
1518 package FS::cust_bill; #!
1519 $FS::cust_bill::_template::address[$l++] =
1520 $cust_main->payname.
1521 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1522 ? " (P.O. #". $cust_main->payinfo. ")"
1526 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1527 if $cust_main->company;
1528 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1529 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1530 if $cust_main->address2;
1531 $FS::cust_bill::_template::address[$l++] =
1532 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1534 my $countrydefault = $conf->config('countrydefault') || 'US';
1535 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1536 unless $cust_main->country eq $countrydefault;
1538 # #overdue? (variable for the template)
1539 # $FS::cust_bill::_template::overdue = (
1541 # && $today > $self->_date
1542 ## && $self->printed > 1
1543 # && $self->printed > 0
1546 #and subroutine for the template
1547 sub FS::cust_bill::_template::invoice_lines {
1548 my $lines = shift || scalar(@buf);
1550 scalar(@buf) ? shift @buf : [ '', '' ];
1556 $FS::cust_bill::_template::page = 1;
1560 push @collect, split("\n",
1561 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1563 $FS::cust_bill::_template::page++;
1566 map "$_\n", @collect;
1570 =item print_latex [ TIME [ , TEMPLATE ] ]
1572 Internal method - returns a filename of a filled-in LaTeX template for this
1573 invoice (Note: add ".tex" to get the actual filename).
1575 See print_ps and print_pdf for methods that return PostScript and PDF output.
1577 TIME an optional value used to control the printing of overdue messages. The
1578 default is now. It isn't the date of the invoice; that's the `_date' field.
1579 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1580 L<Time::Local> and L<Date::Parse> for conversion functions.
1584 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1587 my( $self, $today, $template ) = @_;
1589 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1592 my $cust_main = $self->cust_main;
1593 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1594 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1596 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1597 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1598 #my $balance_due = $self->owed + $pr_total - $cr_total;
1599 my $balance_due = $self->owed + $pr_total;
1601 #create the template
1602 $template ||= $self->_agent_template;
1603 my $templatefile = 'invoice_latex';
1604 my $suffix = length($template) ? "_$template" : '';
1605 $templatefile .= $suffix;
1606 my @invoice_template = map "$_\n", $conf->config($templatefile)
1607 or die "cannot load config file $templatefile";
1609 my($format, $text_template);
1610 if ( grep { /^%%Detail/ } @invoice_template ) {
1611 #change this to a die when the old code is removed
1612 warn "old-style invoice template $templatefile; ".
1613 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1616 $format = 'Text::Template';
1617 $text_template = new Text::Template(
1619 SOURCE => \@invoice_template,
1620 DELIMITERS => [ '[@--', '--@]' ],
1623 $text_template->compile()
1624 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1628 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1629 $returnaddress = join("\n",
1630 $conf->config_orbase('invoice_latexreturnaddress', $template)
1633 $returnaddress = '~';
1636 my %invoice_data = (
1637 'invnum' => $self->invnum,
1638 'date' => time2str('%b %o, %Y', $self->_date),
1639 'today' => time2str('%b %o, %Y', $today),
1640 'agent' => _latex_escape($cust_main->agent->agent),
1641 'payname' => _latex_escape($cust_main->payname),
1642 'company' => _latex_escape($cust_main->company),
1643 'address1' => _latex_escape($cust_main->address1),
1644 'address2' => _latex_escape($cust_main->address2),
1645 'city' => _latex_escape($cust_main->city),
1646 'state' => _latex_escape($cust_main->state),
1647 'zip' => _latex_escape($cust_main->zip),
1648 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1649 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1650 'returnaddress' => $returnaddress,
1652 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1653 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1654 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1657 my $countrydefault = $conf->config('countrydefault') || 'US';
1658 if ( $cust_main->country eq $countrydefault ) {
1659 $invoice_data{'country'} = '';
1661 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1664 $invoice_data{'notes'} =
1666 # #do variable substitutions in notes
1667 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1668 $conf->config_orbase('invoice_latexnotes', $template)
1670 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1673 $invoice_data{'footer'} =~ s/\n+$//;
1674 $invoice_data{'smallfooter'} =~ s/\n+$//;
1675 $invoice_data{'notes'} =~ s/\n+$//;
1677 $invoice_data{'po_line'} =
1678 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1679 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1683 if ( $format eq 'old' ) {
1686 my @total_item = ();
1687 while ( @invoice_template ) {
1688 my $line = shift @invoice_template;
1690 if ( $line =~ /^%%Detail\s*$/ ) {
1692 while ( ( my $line_item_line = shift @invoice_template )
1693 !~ /^%%EndDetail\s*$/ ) {
1694 push @line_item, $line_item_line;
1696 foreach my $line_item ( $self->_items ) {
1697 #foreach my $line_item ( $self->_items_pkg ) {
1698 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1699 $invoice_data{'description'} =
1700 _latex_escape($line_item->{'description'});
1701 if ( exists $line_item->{'ext_description'} ) {
1702 $invoice_data{'description'} .=
1703 "\\tabularnewline\n~~".
1704 join( "\\tabularnewline\n~~",
1705 map _latex_escape($_), @{$line_item->{'ext_description'}}
1708 $invoice_data{'amount'} = $line_item->{'amount'};
1709 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1711 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1714 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1716 while ( ( my $total_item_line = shift @invoice_template )
1717 !~ /^%%EndTotalDetails\s*$/ ) {
1718 push @total_item, $total_item_line;
1721 my @total_fill = ();
1724 foreach my $tax ( $self->_items_tax ) {
1725 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1726 $taxtotal += $tax->{'amount'};
1727 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1729 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1734 $invoice_data{'total_item'} = 'Sub-total';
1735 $invoice_data{'total_amount'} =
1736 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1737 unshift @total_fill,
1738 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1742 $invoice_data{'total_item'} = '\textbf{Total}';
1743 $invoice_data{'total_amount'} =
1744 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1746 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1749 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1752 foreach my $credit ( $self->_items_credits ) {
1753 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1755 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1757 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1762 foreach my $payment ( $self->_items_payments ) {
1763 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1765 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1767 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1771 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1772 $invoice_data{'total_amount'} =
1773 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1775 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1778 push @filled_in, @total_fill;
1781 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1782 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1783 push @filled_in, $line;
1794 } elsif ( $format eq 'Text::Template' ) {
1796 my @detail_items = ();
1797 my @total_items = ();
1799 $invoice_data{'detail_items'} = \@detail_items;
1800 $invoice_data{'total_items'} = \@total_items;
1802 foreach my $line_item ( $self->_items ) {
1804 ext_description => [],
1806 $detail->{'ref'} = $line_item->{'pkgnum'};
1807 $detail->{'quantity'} = 1;
1808 $detail->{'description'} = _latex_escape($line_item->{'description'});
1809 if ( exists $line_item->{'ext_description'} ) {
1810 @{$detail->{'ext_description'}} = map {
1812 } @{$line_item->{'ext_description'}};
1814 $detail->{'amount'} = $line_item->{'amount'};
1815 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1817 push @detail_items, $detail;
1822 foreach my $tax ( $self->_items_tax ) {
1824 $total->{'total_item'} = _latex_escape($tax->{'description'});
1825 $taxtotal += $tax->{'amount'};
1826 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1827 push @total_items, $total;
1832 $total->{'total_item'} = 'Sub-total';
1833 $total->{'total_amount'} =
1834 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1835 unshift @total_items, $total;
1840 $total->{'total_item'} = '\textbf{Total}';
1841 $total->{'total_amount'} =
1842 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1843 push @total_items, $total;
1846 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1849 foreach my $credit ( $self->_items_credits ) {
1851 $total->{'total_item'} = _latex_escape($credit->{'description'});
1853 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1854 push @total_items, $total;
1858 foreach my $payment ( $self->_items_payments ) {
1860 $total->{'total_item'} = _latex_escape($payment->{'description'});
1862 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1863 push @total_items, $total;
1868 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1869 $total->{'total_amount'} =
1870 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1871 push @total_items, $total;
1875 die "guru meditation #54";
1878 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1879 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1883 ) or die "can't open temp file: $!\n";
1884 if ( $format eq 'old' ) {
1885 print $fh join('', @filled_in );
1886 } elsif ( $format eq 'Text::Template' ) {
1887 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1889 die "guru meditation #32";
1893 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1898 =item print_ps [ TIME [ , TEMPLATE ] ]
1900 Returns an postscript invoice, as a scalar.
1902 TIME an optional value used to control the printing of overdue messages. The
1903 default is now. It isn't the date of the invoice; that's the `_date' field.
1904 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1905 L<Time::Local> and L<Date::Parse> for conversion functions.
1912 my $file = $self->print_latex(@_);
1914 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1917 my $sfile = shell_quote $file;
1919 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1920 or die "pslatex $file.tex failed; see $file.log for details?\n";
1921 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1922 or die "pslatex $file.tex failed; see $file.log for details?\n";
1924 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1925 or die "dvips failed";
1927 open(POSTSCRIPT, "<$file.ps")
1928 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1930 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1933 while (<POSTSCRIPT>) {
1943 =item print_pdf [ TIME [ , TEMPLATE ] ]
1945 Returns an PDF invoice, as a scalar.
1947 TIME an optional value used to control the printing of overdue messages. The
1948 default is now. It isn't the date of the invoice; that's the `_date' field.
1949 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1950 L<Time::Local> and L<Date::Parse> for conversion functions.
1957 my $file = $self->print_latex(@_);
1959 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1962 #system('pdflatex', "$file.tex");
1963 #system('pdflatex', "$file.tex");
1964 #! LaTeX Error: Unknown graphics extension: .eps.
1966 my $sfile = shell_quote $file;
1968 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1969 or die "pslatex $file.tex failed; see $file.log for details?\n";
1970 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1971 or die "pslatex $file.tex failed; see $file.log for details?\n";
1973 #system('dvipdf', "$file.dvi", "$file.pdf" );
1975 "dvips -q -t letter -f $sfile.dvi ".
1976 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1979 or die "dvips | gs failed: $!";
1981 open(PDF, "<$file.pdf")
1982 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1984 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1997 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1999 Returns an HTML invoice, as a scalar.
2001 TIME an optional value used to control the printing of overdue messages. The
2002 default is now. It isn't the date of the invoice; that's the `_date' field.
2003 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2004 L<Time::Local> and L<Date::Parse> for conversion functions.
2006 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2007 when emailing the invoice as part of a multipart/related MIME email.
2011 #some falze laziness w/print_text and print_latex (and send_csv)
2013 my( $self, $today, $template, $cid ) = @_;
2016 my $cust_main = $self->cust_main;
2017 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2018 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2020 $template ||= $self->_agent_template;
2021 my $templatefile = 'invoice_html';
2022 my $suffix = length($template) ? "_$template" : '';
2023 $templatefile .= $suffix;
2024 my @html_template = map "$_\n", $conf->config($templatefile)
2025 or die "cannot load config file $templatefile";
2027 my $html_template = new Text::Template(
2029 SOURCE => \@html_template,
2030 DELIMITERS => [ '<%=', '%>' ],
2033 $html_template->compile()
2034 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2036 my %invoice_data = (
2037 'invnum' => $self->invnum,
2038 'date' => time2str('%b %o, %Y', $self->_date),
2039 'today' => time2str('%b %o, %Y', $today),
2040 'agent' => encode_entities($cust_main->agent->agent),
2041 'payname' => encode_entities($cust_main->payname),
2042 'company' => encode_entities($cust_main->company),
2043 'address1' => encode_entities($cust_main->address1),
2044 'address2' => encode_entities($cust_main->address2),
2045 'city' => encode_entities($cust_main->city),
2046 'state' => encode_entities($cust_main->state),
2047 'zip' => encode_entities($cust_main->zip),
2048 'terms' => $conf->config('invoice_default_terms')
2049 || 'Payable upon receipt',
2051 'template' => $template,
2052 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2056 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2057 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2059 $invoice_data{'returnaddress'} =
2060 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2062 $invoice_data{'returnaddress'} =
2065 s/\\\\\*?\s*$/<BR>/;
2066 s/\\hyphenation\{[\w\s\-]+\}//;
2069 $conf->config_orbase( 'invoice_latexreturnaddress',
2075 my $countrydefault = $conf->config('countrydefault') || 'US';
2076 if ( $cust_main->country eq $countrydefault ) {
2077 $invoice_data{'country'} = '';
2079 $invoice_data{'country'} =
2080 encode_entities(code2country($cust_main->country));
2084 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2085 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
2087 $invoice_data{'notes'} =
2088 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2090 $invoice_data{'notes'} =
2092 s/%%(.*)$/<!-- $1 -->/;
2093 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2094 s/\\begin\{enumerate\}/<ol>/;
2096 s/\\end\{enumerate\}/<\/ol>/;
2097 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2100 $conf->config_orbase('invoice_latexnotes', $template)
2104 # #do variable substitutions in notes
2105 # $invoice_data{'notes'} =
2107 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2108 # $conf->config_orbase('invoice_latexnotes', $suffix)
2112 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2113 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
2115 $invoice_data{'footer'} =
2116 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2118 $invoice_data{'footer'} =
2119 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
2120 $conf->config_orbase('invoice_latexfooter', $template)
2124 $invoice_data{'po_line'} =
2125 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2126 ? encode_entities("Purchase Order #". $cust_main->payinfo)
2129 my $money_char = $conf->config('money_char') || '$';
2131 foreach my $line_item ( $self->_items ) {
2133 ext_description => [],
2135 $detail->{'ref'} = $line_item->{'pkgnum'};
2136 $detail->{'description'} = encode_entities($line_item->{'description'});
2137 if ( exists $line_item->{'ext_description'} ) {
2138 @{$detail->{'ext_description'}} = map {
2139 encode_entities($_);
2140 } @{$line_item->{'ext_description'}};
2142 $detail->{'amount'} = $money_char. $line_item->{'amount'};
2143 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2145 push @{$invoice_data{'detail_items'}}, $detail;
2150 foreach my $tax ( $self->_items_tax ) {
2152 $total->{'total_item'} = encode_entities($tax->{'description'});
2153 $taxtotal += $tax->{'amount'};
2154 $total->{'total_amount'} = $money_char. $tax->{'amount'};
2155 push @{$invoice_data{'total_items'}}, $total;
2160 $total->{'total_item'} = 'Sub-total';
2161 $total->{'total_amount'} =
2162 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2163 unshift @{$invoice_data{'total_items'}}, $total;
2166 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2169 $total->{'total_item'} = '<b>Total</b>';
2170 $total->{'total_amount'} =
2171 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2172 push @{$invoice_data{'total_items'}}, $total;
2175 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2178 foreach my $credit ( $self->_items_credits ) {
2180 $total->{'total_item'} = encode_entities($credit->{'description'});
2182 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2183 push @{$invoice_data{'total_items'}}, $total;
2187 foreach my $payment ( $self->_items_payments ) {
2189 $total->{'total_item'} = encode_entities($payment->{'description'});
2191 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2192 push @{$invoice_data{'total_items'}}, $total;
2197 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2198 $total->{'total_amount'} =
2199 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2200 push @{$invoice_data{'total_items'}}, $total;
2203 $html_template->fill_in( HASH => \%invoice_data);
2206 # quick subroutine for print_latex
2208 # There are ten characters that LaTeX treats as special characters, which
2209 # means that they do not simply typeset themselves:
2210 # # $ % & ~ _ ^ \ { }
2212 # TeX ignores blanks following an escaped character; if you want a blank (as
2213 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2217 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2218 $value =~ s/([<>])/\$$1\$/g;
2222 #utility methods for print_*
2224 sub balance_due_msg {
2226 my $msg = 'Balance Due';
2227 return $msg unless $conf->exists('invoice_default_terms');
2228 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2229 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2230 } elsif ( $conf->config('invoice_default_terms') ) {
2231 $msg .= ' - '. $conf->config('invoice_default_terms');
2238 my @display = scalar(@_)
2240 : qw( _items_previous _items_pkg );
2241 #: qw( _items_pkg );
2242 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2244 foreach my $display ( @display ) {
2245 push @b, $self->$display(@_);
2250 sub _items_previous {
2252 my $cust_main = $self->cust_main;
2253 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2255 foreach ( @pr_cust_bill ) {
2257 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2258 ' ('. time2str('%x',$_->_date). ')',
2259 #'pkgpart' => 'N/A',
2261 'amount' => sprintf("%.2f", $_->owed),
2267 # 'description' => 'Previous Balance',
2268 # #'pkgpart' => 'N/A',
2269 # 'pkgnum' => 'N/A',
2270 # 'amount' => sprintf("%10.2f", $pr_total ),
2271 # 'ext_description' => [ map {
2272 # "Invoice ". $_->invnum.
2273 # " (". time2str("%x",$_->_date). ") ".
2274 # sprintf("%10.2f", $_->owed)
2275 # } @pr_cust_bill ],
2282 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2283 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2288 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2289 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2292 sub _items_cust_bill_pkg {
2294 my $cust_bill_pkg = shift;
2297 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2299 my $desc = $cust_bill_pkg->desc;
2301 if ( $cust_bill_pkg->pkgnum > 0 ) {
2303 if ( $cust_bill_pkg->setup != 0 ) {
2304 my $description = $desc;
2305 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2306 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2307 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2309 description => $description,
2310 #pkgpart => $part_pkg->pkgpart,
2311 pkgnum => $cust_bill_pkg->pkgnum,
2312 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2313 ext_description => \@d,
2317 if ( $cust_bill_pkg->recur != 0 ) {
2319 description => "$desc (" .
2320 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2321 time2str('%x', $cust_bill_pkg->edate). ')',
2322 #pkgpart => $part_pkg->pkgpart,
2323 pkgnum => $cust_bill_pkg->pkgnum,
2324 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2326 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2327 $cust_bill_pkg->sdate),
2328 $cust_bill_pkg->details,
2333 } else { #pkgnum tax or one-shot line item (??)
2335 if ( $cust_bill_pkg->setup != 0 ) {
2337 'description' => $desc,
2338 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2341 if ( $cust_bill_pkg->recur != 0 ) {
2343 'description' => "$desc (".
2344 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2345 time2str("%x", $cust_bill_pkg->edate). ')',
2346 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2358 sub _items_credits {
2363 foreach ( $self->cust_credited ) {
2365 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2367 my $reason = $_->cust_credit->reason;
2368 #my $reason = substr($_->cust_credit->reason,0,32);
2369 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2370 $reason = " ($reason) " if $reason;
2372 #'description' => 'Credit ref\#'. $_->crednum.
2373 # " (". time2str("%x",$_->cust_credit->_date) .")".
2375 'description' => 'Credit applied '.
2376 time2str("%x",$_->cust_credit->_date). $reason,
2377 'amount' => sprintf("%.2f",$_->amount),
2380 #foreach ( @cr_cust_credit ) {
2382 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2383 # $money_char. sprintf("%10.2f",$_->credited)
2391 sub _items_payments {
2395 #get & print payments
2396 foreach ( $self->cust_bill_pay ) {
2398 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2401 'description' => "Payment received ".
2402 time2str("%x",$_->cust_pay->_date ),
2403 'amount' => sprintf("%.2f", $_->amount )
2422 sub process_reprint {
2423 process_re_X('print', @_);
2430 sub process_reemail {
2431 process_re_X('email', @_);
2439 process_re_X('fax', @_);
2442 use Storable qw(thaw);
2446 my( $method, $job ) = ( shift, shift );
2448 my $param = thaw(decode_base64(shift));
2449 warn Dumper($param) if $DEBUG;
2460 my($method, $job, %param ) = @_;
2461 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2463 #some false laziness w/search/cust_bill.html
2465 my $orderby = 'ORDER BY cust_bill._date';
2469 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2470 push @where, "cust_bill._date >= $1";
2472 if ( $param{'end'} =~ /^(\d+)$/ ) {
2473 push @where, "cust_bill._date < $1";
2475 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2476 push @where, "cust_main.agentnum = $1";
2480 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2481 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2482 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2483 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2485 push @where, "0 != $owed"
2488 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2491 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2493 my $addl_from = 'left join cust_main using ( custnum )';
2495 if ( $param{'newest_percust'} ) {
2496 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2497 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2498 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2501 my @cust_bill = qsearch( 'cust_bill',
2503 "$distinct cust_bill.*",
2509 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2510 foreach my $cust_bill ( @cust_bill ) {
2511 $cust_bill->$method();
2513 if ( $job ) { #progressbar foo
2515 if ( time - $min_sec > $last ) {
2516 my $error = $job->update_statustext(
2517 int( 100 * $num / scalar(@cust_bill) )
2519 die $error if $error;
2534 print_text formatting (and some logic :/) is in source, but needs to be
2535 slurped in from a file. Also number of lines ($=).
2539 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2540 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base