4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
8 use Text::Template 1.20;
10 use String::ShellQuote;
13 use FS::UID qw( datasrc );
14 use FS::Record qw( qsearch qsearchs );
15 use FS::Misc qw( send_email send_fax );
17 use FS::cust_bill_pkg;
21 use FS::cust_credit_bill;
22 use FS::cust_pay_batch;
23 use FS::cust_bill_event;
25 use FS::cust_bill_pay;
26 use FS::part_bill_event;
28 @ISA = qw( FS::Record );
32 #ask FS::UID to run this stuff for us later
33 FS::UID->install_callback( sub {
35 $money_char = $conf->config('money_char') || '$';
40 FS::cust_bill - Object methods for cust_bill records
46 $record = new FS::cust_bill \%hash;
47 $record = new FS::cust_bill { 'column' => 'value' };
49 $error = $record->insert;
51 $error = $new_record->replace($old_record);
53 $error = $record->delete;
55 $error = $record->check;
57 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
59 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
61 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
63 @cust_pay_objects = $cust_bill->cust_pay;
65 $tax_amount = $record->tax;
67 @lines = $cust_bill->print_text;
68 @lines = $cust_bill->print_text $time;
72 An FS::cust_bill object represents an invoice; a declaration that a customer
73 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
74 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
75 following fields are currently supported:
79 =item invnum - primary key (assigned automatically for new invoices)
81 =item custnum - customer (see L<FS::cust_main>)
83 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
84 L<Time::Local> and L<Date::Parse> for conversion functions.
86 =item charged - amount of this invoice
88 =item printed - deprecated
90 =item closed - books closed flag, empty or `Y'
100 Creates a new invoice. To add the invoice to the database, see L<"insert">.
101 Invoices are normally created by calling the bill method of a customer object
102 (see L<FS::cust_main>).
106 sub table { 'cust_bill'; }
110 Adds this invoice to the database ("Posts" the invoice). If there is an error,
111 returns the error, otherwise returns false.
115 Currently unimplemented. I don't remove invoices because there would then be
116 no record you ever posted this invoice (which is bad, no?)
122 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
123 $self->SUPER::delete(@_);
126 =item replace OLD_RECORD
128 Replaces the OLD_RECORD with this one in the database. If there is an error,
129 returns the error, otherwise returns false.
131 Only printed may be changed. printed is normally updated by calling the
132 collect method of a customer object (see L<FS::cust_main>).
137 my( $new, $old ) = ( shift, shift );
138 return "Can't change custnum!" unless $old->custnum == $new->custnum;
139 #return "Can't change _date!" unless $old->_date eq $new->_date;
140 return "Can't change _date!" unless $old->_date == $new->_date;
141 return "Can't change charged!" unless $old->charged == $new->charged;
143 $new->SUPER::replace($old);
148 Checks all fields to make sure this is a valid invoice. If there is an error,
149 returns the error, otherwise returns false. Called by the insert and replace
158 $self->ut_numbern('invnum')
159 || $self->ut_number('custnum')
160 || $self->ut_numbern('_date')
161 || $self->ut_money('charged')
162 || $self->ut_numbern('printed')
163 || $self->ut_enum('closed', [ '', 'Y' ])
165 return $error if $error;
167 return "Unknown customer"
168 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
170 $self->_date(time) unless $self->_date;
172 $self->printed(0) if $self->printed eq '';
179 Returns a list consisting of the total previous balance for this customer,
180 followed by the previous outstanding invoices (as FS::cust_bill objects also).
187 my @cust_bill = sort { $a->_date <=> $b->_date }
188 grep { $_->owed != 0 && $_->_date < $self->_date }
189 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
191 foreach ( @cust_bill ) { $total += $_->owed; }
197 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
203 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
206 =item cust_bill_event
208 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
213 sub cust_bill_event {
215 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
221 Returns the customer (see L<FS::cust_main>) for this invoice.
227 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
232 Depreciated. See the cust_credited method.
234 #Returns a list consisting of the total previous credited (see
235 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
236 #outstanding credits (FS::cust_credit objects).
242 croak "FS::cust_bill->cust_credit depreciated; see ".
243 "FS::cust_bill->cust_credit_bill";
246 #my @cust_credit = sort { $a->_date <=> $b->_date }
247 # grep { $_->credited != 0 && $_->_date < $self->_date }
248 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
250 #foreach (@cust_credit) { $total += $_->credited; }
251 #$total, @cust_credit;
256 Depreciated. See the cust_bill_pay method.
258 #Returns all payments (see L<FS::cust_pay>) for this invoice.
264 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
266 #sort { $a->_date <=> $b->_date }
267 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
273 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
279 sort { $a->_date <=> $b->_date }
280 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
285 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
291 sort { $a->_date <=> $b->_date }
292 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
298 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
305 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
307 foreach (@taxlines) { $total += $_->setup; }
313 Returns the amount owed (still outstanding) on this invoice, which is charged
314 minus all payment applications (see L<FS::cust_bill_pay>) and credit
315 applications (see L<FS::cust_credit_bill>).
321 my $balance = $self->charged;
322 $balance -= $_->amount foreach ( $self->cust_bill_pay );
323 $balance -= $_->amount foreach ( $self->cust_credited );
324 $balance = sprintf( "%.2f", $balance);
325 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
330 =item generate_email PARAMHASH
332 PARAMHASH can contain the following:
336 =item from => sender address, required
338 =item tempate => alternate template name, optional
340 =item print_text => text attachment arrayref, optional
342 =item subject => email subject, optional
346 Returns an argument list to be passed to L<FS::Misc::send_email>.
357 my $me = '[FS::cust_bill::generate_email]';
360 'from' => $args{'from'},
361 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
364 if (ref($args{'to'} eq 'ARRAY')) {
365 $return{'to'} = $args{'to'};
367 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
368 $self->cust_main->invoicing_list
372 if ( $conf->exists('invoice_html') ) {
374 warn "$me creating HTML/text multipart message"
377 $return{'nobody'} = 1;
379 my $alternative = build MIME::Entity
380 'Type' => 'multipart/alternative',
381 'Encoding' => '7bit',
382 'Disposition' => 'inline'
386 if ( $conf->exists('invoice_email_pdf')
387 and scalar($conf->config('invoice_email_pdf_note')) ) {
389 warn "$me using 'invoice_email_pdf_note' in multipart message"
391 $data = [ map { $_ . "\n" }
392 $conf->config('invoice_email_pdf_note')
397 warn "$me not using 'invoice_email_pdf_note' in multipart message"
399 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
400 $data = $args{'print_text'};
402 $data = [ $self->print_text('', $args{'template'}) ];
407 $alternative->attach(
408 'Type' => 'text/plain',
409 #'Encoding' => 'quoted-printable',
410 'Encoding' => '7bit',
412 'Disposition' => 'inline',
415 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
416 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
418 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
420 if ( length($args{'_template'})
421 && -e "$path/logo_". $args{'_template'}. ".png"
424 $file = "$path/logo_". $args{'_template'}. ".png";
426 $file = "$path/logo.png";
429 my $image = build MIME::Entity
430 'Type' => 'image/png',
431 'Encoding' => 'base64',
433 'Filename' => 'logo.png',
434 'Content-ID' => "<$content_id>",
437 $alternative->attach(
438 'Type' => 'text/html',
439 'Encoding' => 'quoted-printable',
440 'Data' => [ '<html>',
443 ' '. encode_entities($return{'subject'}),
446 ' <body bgcolor="#e8e8e8">',
447 $self->print_html('', $args{'template'}, $content_id),
451 'Disposition' => 'inline',
452 #'Filename' => 'invoice.pdf',
455 if ( $conf->exists('invoice_email_pdf') ) {
460 # multipart/alternative
466 my $related = build MIME::Entity 'Type' => 'multipart/related',
467 'Encoding' => '7bit';
469 #false laziness w/Misc::send_email
470 $related->head->replace('Content-type',
472 '; boundary="'. $related->head->multipart_boundary. '"'.
473 '; type=multipart/alternative'
476 $related->add_part($alternative);
478 $related->add_part($image);
480 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
482 $return{'mimeparts'} = [ $related, $pdf ];
486 #no other attachment:
488 # multipart/alternative
493 $return{'content-type'} = 'multipart/related';
494 $return{'mimeparts'} = [ $alternative, $image ];
495 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
496 #$return{'disposition'} = 'inline';
502 if ( $conf->exists('invoice_email_pdf') ) {
503 warn "$me creating PDF attachment"
506 #mime parts arguments a la MIME::Entity->build().
507 $return{'mimeparts'} = [
508 { $self->mimebuild_pdf('', $args{'template'}) }
512 if ( $conf->exists('invoice_email_pdf')
513 and scalar($conf->config('invoice_email_pdf_note')) ) {
515 warn "$me using 'invoice_email_pdf_note'"
517 $return{'body'} = [ map { $_ . "\n" }
518 $conf->config('invoice_email_pdf_note')
523 warn "$me not using 'invoice_email_pdf_note'"
525 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
526 $return{'body'} = $args{'print_text'};
528 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
541 Returns a list suitable for passing to MIME::Entity->build(), representing
542 this invoice as PDF attachment.
549 'Type' => 'application/pdf',
550 'Encoding' => 'base64',
551 'Data' => [ $self->print_pdf(@_) ],
552 'Disposition' => 'attachment',
553 'Filename' => 'invoice.pdf',
557 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
559 Sends this invoice to the destinations configured for this customer: sends
560 email, prints and/or faxes. See L<FS::cust_main_invoice>.
562 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
564 AGENTNUM, if specified, means that this invoice will only be sent for customers
565 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
566 single agent) or an arrayref of agentnums.
568 INVOICE_FROM, if specified, overrides the default email invoice From: address.
574 my $template = scalar(@_) ? shift : '';
575 if ( scalar(@_) && $_[0] ) {
576 my $agentnums = ref($_[0]) ? shift : [ shift ];
577 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
583 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
585 my @invoicing_list = $self->cust_main->invoicing_list;
587 $self->email($template, $invoice_from)
588 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
590 $self->print($template)
591 if grep { $_ eq 'POST' } @invoicing_list; #postal
593 $self->fax($template)
594 if grep { $_ eq 'FAX' } @invoicing_list; #fax
600 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
604 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
606 INVOICE_FROM, if specified, overrides the default email invoice From: address.
612 my $template = scalar(@_) ? shift : '';
616 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
618 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
619 $self->cust_main->invoicing_list;
621 #better to notify this person than silence
622 @invoicing_list = ($invoice_from) unless @invoicing_list;
624 my $error = send_email(
625 $self->generate_email(
626 'from' => $invoice_from,
627 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
628 'template' => $template,
631 die "can't email invoice: $error\n" if $error;
632 #die "$error\n" if $error;
636 =item lpr_data [ TEMPLATENAME ]
638 Returns the postscript or plaintext for this invoice as an arrayref.
640 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
645 my( $self, $template) = @_;
646 $conf->exists('invoice_latex')
647 ? [ $self->print_ps('', $template) ]
648 : [ $self->print_text('', $template) ];
651 =item print [ TEMPLATENAME ]
655 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
661 my $template = scalar(@_) ? shift : '';
663 my $lpr = $conf->config('lpr');
666 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
668 $outerr = ": $outerr" if length($outerr);
669 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
674 =item fax [ TEMPLATENAME ]
678 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
684 my $template = scalar(@_) ? shift : '';
686 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
687 unless $conf->exists('invoice_latex');
689 my $dialstring = $self->cust_main->getfield('fax');
692 my $error = send_fax( 'docdata' => $self->lpr_data($template),
693 'dialstring' => $dialstring,
695 die $error if $error;
699 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
701 Like B<send>, but only sends the invoice if it is the newest open invoice for
711 grep { $_->owed > 0 }
712 qsearch('cust_bill', {
713 'custnum' => $self->custnum,
714 #'_date' => { op=>'>', value=>$self->_date },
715 'invnum' => { op=>'>', value=>$self->invnum },
722 =item send_csv OPTIONS
724 Sends invoice as a CSV data-file to a remote host with the specified protocol.
728 protocol - currently only "ftp"
734 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
735 and YYMMDDHHMMSS is a timestamp.
737 The fields of the CSV file is as follows:
739 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
743 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
745 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
746 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
747 fields are filled in.
749 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
750 first two fields (B<record_type> and B<invnum>) and the last five fields
751 (B<pkg> through B<edate>) are filled in.
753 =item invnum - invoice number
755 =item custnum - customer number
757 =item _date - invoice date
759 =item charged - total invoice amount
761 =item first - customer first name
763 =item last - customer first name
765 =item company - company name
767 =item address1 - address line 1
769 =item address2 - address line 1
779 =item pkg - line item description
781 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
783 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
785 =item sdate - start date for recurring fee
787 =item edate - end date for recurring fee
794 my($self, %opt) = @_;
796 #part one: create file
798 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
799 mkdir $spooldir, 0700 unless -d $spooldir;
801 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
803 open(CSV, ">$file") or die "can't open $file: $!";
805 eval "use Text::CSV_XS";
808 my $csv = Text::CSV_XS->new({'always_quote'=>1});
810 my $cust_main = $self->cust_main;
816 time2str("%x", $self->_date),
817 sprintf("%.2f", $self->charged),
818 ( map { $cust_main->getfield($_) }
819 qw( first last company address1 address2 city state zip country ) ),
821 ) or die "can't create csv";
822 print CSV $csv->string. "\n";
824 #new charges (false laziness w/print_text and _items stuff)
825 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
827 my($pkg, $setup, $recur, $sdate, $edate);
828 if ( $cust_bill_pkg->pkgnum ) {
830 ($pkg, $setup, $recur, $sdate, $edate) = (
831 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
832 ( $cust_bill_pkg->setup != 0
833 ? sprintf("%.2f", $cust_bill_pkg->setup )
835 ( $cust_bill_pkg->recur != 0
836 ? sprintf("%.2f", $cust_bill_pkg->recur )
838 time2str("%x", $cust_bill_pkg->sdate),
839 time2str("%x", $cust_bill_pkg->edate),
843 next unless $cust_bill_pkg->setup != 0;
844 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
845 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
847 ($pkg, $setup, $recur, $sdate, $edate) =
848 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
854 ( map { '' } (1..11) ),
855 ($pkg, $setup, $recur, $sdate, $edate)
856 ) or die "can't create csv";
857 print CSV $csv->string. "\n";
861 close CSV or die "can't close CSV: $!";
866 if ( $opt{protocol} eq 'ftp' ) {
867 eval "use Net::FTP;";
869 $net = Net::FTP->new($opt{server}) or die @$;
871 die "unknown protocol: $opt{protocol}";
874 $net->login( $opt{username}, $opt{password} )
875 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
877 $net->binary or die "can't set binary mode";
879 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
881 $net->put($file) or die "can't put $file: $!";
891 Pays this invoice with a compliemntary payment. If there is an error,
892 returns the error, otherwise returns false.
898 my $cust_pay = new FS::cust_pay ( {
899 'invnum' => $self->invnum,
900 'paid' => $self->owed,
903 'payinfo' => $self->cust_main->payinfo,
911 Attempts to pay this invoice with a credit card payment via a
912 Business::OnlinePayment realtime gateway. See
913 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
914 for supported processors.
920 $self->realtime_bop( 'CC', @_ );
925 Attempts to pay this invoice with an electronic check (ACH) payment via a
926 Business::OnlinePayment realtime gateway. See
927 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
928 for supported processors.
934 $self->realtime_bop( 'ECHECK', @_ );
939 Attempts to pay this invoice with phone bill (LEC) payment via a
940 Business::OnlinePayment realtime gateway. See
941 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
942 for supported processors.
948 $self->realtime_bop( 'LEC', @_ );
952 my( $self, $method ) = @_;
954 my $cust_main = $self->cust_main;
955 my $balance = $cust_main->balance;
956 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
957 $amount = sprintf("%.2f", $amount);
958 return "not run (balance $balance)" unless $amount > 0;
960 my $description = 'Internet Services';
961 if ( $conf->exists('business-onlinepayment-description') ) {
962 my $dtempl = $conf->config('business-onlinepayment-description');
964 my $agent_obj = $cust_main->agent
965 or die "can't retreive agent for $cust_main (agentnum ".
966 $cust_main->agentnum. ")";
967 my $agent = $agent_obj->agent;
968 my $pkgs = join(', ',
969 map { $_->cust_pkg->part_pkg->pkg }
970 grep { $_->pkgnum } $self->cust_bill_pkg
972 $description = eval qq("$dtempl");
975 $cust_main->realtime_bop($method, $amount,
976 'description' => $description,
977 'invnum' => $self->invnum,
984 Adds a payment for this invoice to the pending credit card batch (see
985 L<FS::cust_pay_batch>).
991 my $cust_main = $self->cust_main;
993 my $cust_pay_batch = new FS::cust_pay_batch ( {
994 'invnum' => $self->getfield('invnum'),
995 'custnum' => $cust_main->getfield('custnum'),
996 'last' => $cust_main->getfield('last'),
997 'first' => $cust_main->getfield('first'),
998 'address1' => $cust_main->getfield('address1'),
999 'address2' => $cust_main->getfield('address2'),
1000 'city' => $cust_main->getfield('city'),
1001 'state' => $cust_main->getfield('state'),
1002 'zip' => $cust_main->getfield('zip'),
1003 'country' => $cust_main->getfield('country'),
1004 'cardnum' => $cust_main->payinfo,
1005 'exp' => $cust_main->getfield('paydate'),
1006 'payname' => $cust_main->getfield('payname'),
1007 'amount' => $self->owed,
1009 my $error = $cust_pay_batch->insert;
1010 die $error if $error;
1015 sub _agent_template {
1017 $self->_agent_plandata('agent_templatename');
1020 sub _agent_invoice_from {
1022 $self->_agent_plandata('agent_invoice_from');
1025 sub _agent_plandata {
1026 my( $self, $option ) = @_;
1028 my $part_bill_event = qsearchs( 'part_bill_event',
1030 'payby' => $self->cust_main->payby,
1031 'plan' => 'send_agent',
1032 'plandata' => { 'op' => '~',
1033 'value' => "(^|\n)agentnum ".
1035 $self->cust_main->agentnum.
1041 'ORDER BY seconds LIMIT 1'
1044 return '' unless $part_bill_event;
1046 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1049 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1050 " plandata for $option";
1056 =item print_text [ TIME [ , TEMPLATE ] ]
1058 Returns an text invoice, as a list of lines.
1060 TIME an optional value used to control the printing of overdue messages. The
1061 default is now. It isn't the date of the invoice; that's the `_date' field.
1062 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1063 L<Time::Local> and L<Date::Parse> for conversion functions.
1067 #still some false laziness w/_items stuff (and send_csv)
1070 my( $self, $today, $template ) = @_;
1073 # my $invnum = $self->invnum;
1074 my $cust_main = $self->cust_main;
1075 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1076 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1078 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1079 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1080 #my $balance_due = $self->owed + $pr_total - $cr_total;
1081 my $balance_due = $self->owed + $pr_total;
1084 #my($description,$amount);
1088 foreach ( @pr_cust_bill ) {
1090 "Previous Balance, Invoice #". $_->invnum.
1091 " (". time2str("%x",$_->_date). ")",
1092 $money_char. sprintf("%10.2f",$_->owed)
1095 if (@pr_cust_bill) {
1096 push @buf,['','-----------'];
1097 push @buf,[ 'Total Previous Balance',
1098 $money_char. sprintf("%10.2f",$pr_total ) ];
1103 foreach my $cust_bill_pkg (
1104 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1105 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1108 my $desc = $cust_bill_pkg->desc;
1110 if ( $cust_bill_pkg->pkgnum > 0 ) {
1112 if ( $cust_bill_pkg->setup != 0 ) {
1113 my $description = $desc;
1114 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1115 push @buf, [ $description,
1116 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1118 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1119 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1122 if ( $cust_bill_pkg->recur != 0 ) {
1124 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1125 time2str("%x", $cust_bill_pkg->edate) . ")",
1126 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1129 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1130 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1131 $cust_bill_pkg->sdate );
1134 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1136 } else { #pkgnum tax or one-shot line item
1138 if ( $cust_bill_pkg->setup != 0 ) {
1140 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1142 if ( $cust_bill_pkg->recur != 0 ) {
1143 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1144 . time2str("%x", $cust_bill_pkg->edate). ")",
1145 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1153 push @buf,['','-----------'];
1154 push @buf,['Total New Charges',
1155 $money_char. sprintf("%10.2f",$self->charged) ];
1158 push @buf,['','-----------'];
1159 push @buf,['Total Charges',
1160 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1164 foreach ( $self->cust_credited ) {
1166 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1168 my $reason = substr($_->cust_credit->reason,0,32);
1169 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1170 $reason = " ($reason) " if $reason;
1172 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1174 $money_char. sprintf("%10.2f",$_->amount)
1177 #foreach ( @cr_cust_credit ) {
1179 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1180 # $money_char. sprintf("%10.2f",$_->credited)
1184 #get & print payments
1185 foreach ( $self->cust_bill_pay ) {
1187 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1190 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1191 $money_char. sprintf("%10.2f",$_->amount )
1196 my $balance_due_msg = $self->balance_due_msg;
1198 push @buf,['','-----------'];
1199 push @buf,[$balance_due_msg, $money_char.
1200 sprintf("%10.2f", $balance_due ) ];
1202 #create the template
1203 $template ||= $self->_agent_template;
1204 my $templatefile = 'invoice_template';
1205 $templatefile .= "_$template" if length($template);
1206 my @invoice_template = $conf->config($templatefile)
1207 or die "cannot load config file $templatefile";
1210 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1211 /invoice_lines\((\d*)\)/;
1212 $invoice_lines += $1 || scalar(@buf);
1215 die "no invoice_lines() functions in template?" unless $wasfunc;
1216 my $invoice_template = new Text::Template (
1218 SOURCE => [ map "$_\n", @invoice_template ],
1219 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1220 $invoice_template->compile()
1221 or die "can't compile template: $Text::Template::ERROR";
1223 #setup template variables
1224 package FS::cust_bill::_template; #!
1225 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1227 $invnum = $self->invnum;
1228 $date = $self->_date;
1230 $agent = $self->cust_main->agent->agent;
1232 if ( $FS::cust_bill::invoice_lines ) {
1234 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1236 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1241 #format address (variable for the template)
1243 @address = ( '', '', '', '', '', '' );
1244 package FS::cust_bill; #!
1245 $FS::cust_bill::_template::address[$l++] =
1246 $cust_main->payname.
1247 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1248 ? " (P.O. #". $cust_main->payinfo. ")"
1252 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1253 if $cust_main->company;
1254 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1255 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1256 if $cust_main->address2;
1257 $FS::cust_bill::_template::address[$l++] =
1258 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1260 my $countrydefault = $conf->config('countrydefault') || 'US';
1261 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1262 unless $cust_main->country eq $countrydefault;
1264 # #overdue? (variable for the template)
1265 # $FS::cust_bill::_template::overdue = (
1267 # && $today > $self->_date
1268 ## && $self->printed > 1
1269 # && $self->printed > 0
1272 #and subroutine for the template
1273 sub FS::cust_bill::_template::invoice_lines {
1274 my $lines = shift || scalar(@buf);
1276 scalar(@buf) ? shift @buf : [ '', '' ];
1282 $FS::cust_bill::_template::page = 1;
1286 push @collect, split("\n",
1287 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1289 $FS::cust_bill::_template::page++;
1292 map "$_\n", @collect;
1296 =item print_latex [ TIME [ , TEMPLATE ] ]
1298 Internal method - returns a filename of a filled-in LaTeX template for this
1299 invoice (Note: add ".tex" to get the actual filename).
1301 See print_ps and print_pdf for methods that return PostScript and PDF output.
1303 TIME an optional value used to control the printing of overdue messages. The
1304 default is now. It isn't the date of the invoice; that's the `_date' field.
1305 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1306 L<Time::Local> and L<Date::Parse> for conversion functions.
1310 #still some false laziness w/print_text (mostly print_text should use _items stuff though)
1313 my( $self, $today, $template ) = @_;
1315 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1318 my $cust_main = $self->cust_main;
1319 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1320 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1322 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1323 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1324 #my $balance_due = $self->owed + $pr_total - $cr_total;
1325 my $balance_due = $self->owed + $pr_total;
1327 #create the template
1328 $template ||= $self->_agent_template;
1329 my $templatefile = 'invoice_latex';
1330 my $suffix = length($template) ? "_$template" : '';
1331 $templatefile .= $suffix;
1332 my @invoice_template = map "$_\n", $conf->config($templatefile)
1333 or die "cannot load config file $templatefile";
1335 my($format, $text_template);
1336 if ( grep { /^%%Detail/ } @invoice_template ) {
1337 #change this to a die when the old code is removed
1338 warn "old-style invoice template $templatefile; ".
1339 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1342 $format = 'Text::Template';
1343 $text_template = new Text::Template(
1345 SOURCE => \@invoice_template,
1346 DELIMITERS => [ '[@--', '--@]' ],
1349 $text_template->compile()
1350 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1354 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1355 $returnaddress = join("\n",
1356 $conf->config_orbase('invoice_latexreturnaddress', $template)
1359 $returnaddress = '~';
1362 my %invoice_data = (
1363 'invnum' => $self->invnum,
1364 'date' => time2str('%b %o, %Y', $self->_date),
1365 'today' => time2str('%b %o, %Y', $today),
1366 'agent' => _latex_escape($cust_main->agent->agent),
1367 'payname' => _latex_escape($cust_main->payname),
1368 'company' => _latex_escape($cust_main->company),
1369 'address1' => _latex_escape($cust_main->address1),
1370 'address2' => _latex_escape($cust_main->address2),
1371 'city' => _latex_escape($cust_main->city),
1372 'state' => _latex_escape($cust_main->state),
1373 'zip' => _latex_escape($cust_main->zip),
1374 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1375 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1376 'returnaddress' => $returnaddress,
1378 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1379 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1380 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1383 my $countrydefault = $conf->config('countrydefault') || 'US';
1384 if ( $cust_main->country eq $countrydefault ) {
1385 $invoice_data{'country'} = '';
1387 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1390 $invoice_data{'notes'} =
1392 # #do variable substitutions in notes
1393 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1394 $conf->config_orbase('invoice_latexnotes', $template)
1396 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1399 $invoice_data{'footer'} =~ s/\n+$//;
1400 $invoice_data{'smallfooter'} =~ s/\n+$//;
1401 $invoice_data{'notes'} =~ s/\n+$//;
1403 $invoice_data{'po_line'} =
1404 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1405 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1409 if ( $format eq 'old' ) {
1412 my @total_item = ();
1413 while ( @invoice_template ) {
1414 my $line = shift @invoice_template;
1416 if ( $line =~ /^%%Detail\s*$/ ) {
1418 while ( ( my $line_item_line = shift @invoice_template )
1419 !~ /^%%EndDetail\s*$/ ) {
1420 push @line_item, $line_item_line;
1422 foreach my $line_item ( $self->_items ) {
1423 #foreach my $line_item ( $self->_items_pkg ) {
1424 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1425 $invoice_data{'description'} =
1426 _latex_escape($line_item->{'description'});
1427 if ( exists $line_item->{'ext_description'} ) {
1428 $invoice_data{'description'} .=
1429 "\\tabularnewline\n~~".
1430 join( "\\tabularnewline\n~~",
1431 map _latex_escape($_), @{$line_item->{'ext_description'}}
1434 $invoice_data{'amount'} = $line_item->{'amount'};
1435 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1437 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1440 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1442 while ( ( my $total_item_line = shift @invoice_template )
1443 !~ /^%%EndTotalDetails\s*$/ ) {
1444 push @total_item, $total_item_line;
1447 my @total_fill = ();
1450 foreach my $tax ( $self->_items_tax ) {
1451 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1452 $taxtotal += $tax->{'amount'};
1453 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1455 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1460 $invoice_data{'total_item'} = 'Sub-total';
1461 $invoice_data{'total_amount'} =
1462 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1463 unshift @total_fill,
1464 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1468 $invoice_data{'total_item'} = '\textbf{Total}';
1469 $invoice_data{'total_amount'} =
1470 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1472 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1475 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1478 foreach my $credit ( $self->_items_credits ) {
1479 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1481 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1483 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1488 foreach my $payment ( $self->_items_payments ) {
1489 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1491 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1493 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1497 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1498 $invoice_data{'total_amount'} =
1499 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1501 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1504 push @filled_in, @total_fill;
1507 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1508 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1509 push @filled_in, $line;
1520 } elsif ( $format eq 'Text::Template' ) {
1522 my @detail_items = ();
1523 my @total_items = ();
1525 $invoice_data{'detail_items'} = \@detail_items;
1526 $invoice_data{'total_items'} = \@total_items;
1528 foreach my $line_item ( $self->_items ) {
1530 ext_description => [],
1532 $detail->{'ref'} = $line_item->{'pkgnum'};
1533 $detail->{'quantity'} = 1;
1534 $detail->{'description'} = _latex_escape($line_item->{'description'});
1535 if ( exists $line_item->{'ext_description'} ) {
1536 @{$detail->{'ext_description'}} = map {
1538 } @{$line_item->{'ext_description'}};
1540 $detail->{'amount'} = $line_item->{'amount'};
1541 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1543 push @detail_items, $detail;
1548 foreach my $tax ( $self->_items_tax ) {
1550 $total->{'total_item'} = _latex_escape($tax->{'description'});
1551 $taxtotal += $tax->{'amount'};
1552 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1553 push @total_items, $total;
1558 $total->{'total_item'} = 'Sub-total';
1559 $total->{'total_amount'} =
1560 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1561 unshift @total_items, $total;
1566 $total->{'total_item'} = '\textbf{Total}';
1567 $total->{'total_amount'} =
1568 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1569 push @total_items, $total;
1572 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1575 foreach my $credit ( $self->_items_credits ) {
1577 $total->{'total_item'} = _latex_escape($credit->{'description'});
1579 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1580 push @total_items, $total;
1584 foreach my $payment ( $self->_items_payments ) {
1586 $total->{'total_item'} = _latex_escape($payment->{'description'});
1588 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1589 push @total_items, $total;
1594 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1595 $total->{'total_amount'} =
1596 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1597 push @total_items, $total;
1601 die "guru meditation #54";
1604 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1605 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1609 ) or die "can't open temp file: $!\n";
1610 if ( $format eq 'old' ) {
1611 print $fh join('', @filled_in );
1612 } elsif ( $format eq 'Text::Template' ) {
1613 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1615 die "guru meditation #32";
1619 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1624 =item print_ps [ TIME [ , TEMPLATE ] ]
1626 Returns an postscript invoice, as a scalar.
1628 TIME an optional value used to control the printing of overdue messages. The
1629 default is now. It isn't the date of the invoice; that's the `_date' field.
1630 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1631 L<Time::Local> and L<Date::Parse> for conversion functions.
1638 my $file = $self->print_latex(@_);
1640 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1643 my $sfile = shell_quote $file;
1645 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1646 or die "pslatex $file.tex failed; see $file.log for details?\n";
1647 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1648 or die "pslatex $file.tex failed; see $file.log for details?\n";
1650 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1651 or die "dvips failed";
1653 open(POSTSCRIPT, "<$file.ps")
1654 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1656 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1659 while (<POSTSCRIPT>) {
1669 =item print_pdf [ TIME [ , TEMPLATE ] ]
1671 Returns an PDF invoice, as a scalar.
1673 TIME an optional value used to control the printing of overdue messages. The
1674 default is now. It isn't the date of the invoice; that's the `_date' field.
1675 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1676 L<Time::Local> and L<Date::Parse> for conversion functions.
1683 my $file = $self->print_latex(@_);
1685 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1688 #system('pdflatex', "$file.tex");
1689 #system('pdflatex', "$file.tex");
1690 #! LaTeX Error: Unknown graphics extension: .eps.
1692 my $sfile = shell_quote $file;
1694 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1695 or die "pslatex $file.tex failed; see $file.log for details?\n";
1696 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1697 or die "pslatex $file.tex failed; see $file.log for details?\n";
1699 #system('dvipdf', "$file.dvi", "$file.pdf" );
1701 "dvips -q -t letter -f $sfile.dvi ".
1702 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1705 or die "dvips | gs failed: $!";
1707 open(PDF, "<$file.pdf")
1708 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1710 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1723 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1725 Returns an HTML invoice, as a scalar.
1727 TIME an optional value used to control the printing of overdue messages. The
1728 default is now. It isn't the date of the invoice; that's the `_date' field.
1729 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1730 L<Time::Local> and L<Date::Parse> for conversion functions.
1732 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1733 when emailing the invoice as part of a multipart/related MIME email.
1738 my( $self, $today, $template, $cid ) = @_;
1741 my $cust_main = $self->cust_main;
1742 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1743 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1745 $template ||= $self->_agent_template;
1746 my $templatefile = 'invoice_html';
1747 my $suffix = length($template) ? "_$template" : '';
1748 $templatefile .= $suffix;
1749 my @html_template = map "$_\n", $conf->config($templatefile)
1750 or die "cannot load config file $templatefile";
1752 my $html_template = new Text::Template(
1754 SOURCE => \@html_template,
1755 DELIMITERS => [ '<%=', '%>' ],
1758 $html_template->compile()
1759 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1761 my %invoice_data = (
1762 'invnum' => $self->invnum,
1763 'date' => time2str('%b %o, %Y', $self->_date),
1764 'today' => time2str('%b %o, %Y', $today),
1765 'agent' => encode_entities($cust_main->agent->agent),
1766 'payname' => encode_entities($cust_main->payname),
1767 'company' => encode_entities($cust_main->company),
1768 'address1' => encode_entities($cust_main->address1),
1769 'address2' => encode_entities($cust_main->address2),
1770 'city' => encode_entities($cust_main->city),
1771 'state' => encode_entities($cust_main->state),
1772 'zip' => encode_entities($cust_main->zip),
1773 'terms' => $conf->config('invoice_default_terms')
1774 || 'Payable upon receipt',
1776 'template' => $template,
1777 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1780 $invoice_data{'returnaddress'} =
1781 length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1782 ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1785 s/\\\\\*?\s*$/<BR>/;
1786 s/\\hyphenation\{[\w\s\-]+\}//;
1789 $conf->config_orbase('invoice_latexreturnaddress', $template)
1792 my $countrydefault = $conf->config('countrydefault') || 'US';
1793 if ( $cust_main->country eq $countrydefault ) {
1794 $invoice_data{'country'} = '';
1796 $invoice_data{'country'} =
1797 encode_entities(code2country($cust_main->country));
1800 $invoice_data{'notes'} =
1801 length($conf->config_orbase('invoice_htmlnotes', $template))
1802 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1804 s/%%(.*)$/<!-- $1 -->/;
1805 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1806 s/\\begin\{enumerate\}/<ol>/;
1808 s/\\end\{enumerate\}/<\/ol>/;
1809 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1812 $conf->config_orbase('invoice_latexnotes', $template)
1815 # #do variable substitutions in notes
1816 # $invoice_data{'notes'} =
1818 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1819 # $conf->config_orbase('invoice_latexnotes', $suffix)
1822 $invoice_data{'footer'} =
1823 length($conf->config_orbase('invoice_htmlfooter', $template))
1824 ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) )
1825 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1826 $conf->config_orbase('invoice_latexfooter', $template)
1829 $invoice_data{'po_line'} =
1830 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1831 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1834 my $money_char = $conf->config('money_char') || '$';
1836 foreach my $line_item ( $self->_items ) {
1838 ext_description => [],
1840 $detail->{'ref'} = $line_item->{'pkgnum'};
1841 $detail->{'description'} = encode_entities($line_item->{'description'});
1842 if ( exists $line_item->{'ext_description'} ) {
1843 @{$detail->{'ext_description'}} = map {
1844 encode_entities($_);
1845 } @{$line_item->{'ext_description'}};
1847 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1848 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1850 push @{$invoice_data{'detail_items'}}, $detail;
1855 foreach my $tax ( $self->_items_tax ) {
1857 $total->{'total_item'} = encode_entities($tax->{'description'});
1858 $taxtotal += $tax->{'amount'};
1859 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1860 push @{$invoice_data{'total_items'}}, $total;
1865 $total->{'total_item'} = 'Sub-total';
1866 $total->{'total_amount'} =
1867 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1868 unshift @{$invoice_data{'total_items'}}, $total;
1871 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1874 $total->{'total_item'} = '<b>Total</b>';
1875 $total->{'total_amount'} =
1876 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1877 push @{$invoice_data{'total_items'}}, $total;
1880 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1883 foreach my $credit ( $self->_items_credits ) {
1885 $total->{'total_item'} = encode_entities($credit->{'description'});
1887 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1888 push @{$invoice_data{'total_items'}}, $total;
1892 foreach my $payment ( $self->_items_payments ) {
1894 $total->{'total_item'} = encode_entities($payment->{'description'});
1896 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1897 push @{$invoice_data{'total_items'}}, $total;
1902 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1903 $total->{'total_amount'} =
1904 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1905 push @{$invoice_data{'total_items'}}, $total;
1908 $html_template->fill_in( HASH => \%invoice_data);
1911 # quick subroutine for print_latex
1913 # There are ten characters that LaTeX treats as special characters, which
1914 # means that they do not simply typeset themselves:
1915 # # $ % & ~ _ ^ \ { }
1917 # TeX ignores blanks following an escaped character; if you want a blank (as
1918 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1922 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1923 $value =~ s/([<>])/\$$1\$/g;
1927 #utility methods for print_*
1929 sub balance_due_msg {
1931 my $msg = 'Balance Due';
1932 return $msg unless $conf->exists('invoice_default_terms');
1933 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1934 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1935 } elsif ( $conf->config('invoice_default_terms') ) {
1936 $msg .= ' - '. $conf->config('invoice_default_terms');
1943 my @display = scalar(@_)
1945 : qw( _items_previous _items_pkg );
1946 #: qw( _items_pkg );
1947 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1949 foreach my $display ( @display ) {
1950 push @b, $self->$display(@_);
1955 sub _items_previous {
1957 my $cust_main = $self->cust_main;
1958 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1960 foreach ( @pr_cust_bill ) {
1962 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1963 ' ('. time2str('%x',$_->_date). ')',
1964 #'pkgpart' => 'N/A',
1966 'amount' => sprintf("%.2f", $_->owed),
1972 # 'description' => 'Previous Balance',
1973 # #'pkgpart' => 'N/A',
1974 # 'pkgnum' => 'N/A',
1975 # 'amount' => sprintf("%10.2f", $pr_total ),
1976 # 'ext_description' => [ map {
1977 # "Invoice ". $_->invnum.
1978 # " (". time2str("%x",$_->_date). ") ".
1979 # sprintf("%10.2f", $_->owed)
1980 # } @pr_cust_bill ],
1987 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1988 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1993 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1994 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1997 sub _items_cust_bill_pkg {
1999 my $cust_bill_pkg = shift;
2002 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2004 my $desc = $cust_bill_pkg->desc;
2006 if ( $cust_bill_pkg->pkgnum > 0 ) {
2008 if ( $cust_bill_pkg->setup != 0 ) {
2009 my $description = $desc;
2010 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2011 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2012 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2014 description => $description,
2015 #pkgpart => $part_pkg->pkgpart,
2016 pkgnum => $cust_bill_pkg->pkgnum,
2017 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2018 ext_description => \@d,
2022 if ( $cust_bill_pkg->recur != 0 ) {
2024 description => "$desc (" .
2025 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2026 time2str('%x', $cust_bill_pkg->edate). ')',
2027 #pkgpart => $part_pkg->pkgpart,
2028 pkgnum => $cust_bill_pkg->pkgnum,
2029 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2031 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2032 $cust_bill_pkg->sdate),
2033 $cust_bill_pkg->details,
2038 } else { #pkgnum tax or one-shot line item (??)
2040 if ( $cust_bill_pkg->setup != 0 ) {
2042 'description' => $desc,
2043 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2046 if ( $cust_bill_pkg->recur != 0 ) {
2048 'description' => "$desc (".
2049 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2050 time2str("%x", $cust_bill_pkg->edate). ')',
2051 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2063 sub _items_credits {
2068 foreach ( $self->cust_credited ) {
2070 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2072 my $reason = $_->cust_credit->reason;
2073 #my $reason = substr($_->cust_credit->reason,0,32);
2074 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2075 $reason = " ($reason) " if $reason;
2077 #'description' => 'Credit ref\#'. $_->crednum.
2078 # " (". time2str("%x",$_->cust_credit->_date) .")".
2080 'description' => 'Credit applied '.
2081 time2str("%x",$_->cust_credit->_date). $reason,
2082 'amount' => sprintf("%.2f",$_->amount),
2085 #foreach ( @cr_cust_credit ) {
2087 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2088 # $money_char. sprintf("%10.2f",$_->credited)
2096 sub _items_payments {
2100 #get & print payments
2101 foreach ( $self->cust_bill_pay ) {
2103 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2106 'description' => "Payment received ".
2107 time2str("%x",$_->cust_pay->_date ),
2108 'amount' => sprintf("%.2f", $_->amount )
2126 sub process_reprint {
2127 process_re_X('print', @_);
2134 sub process_reemail {
2135 process_re_X('email', @_);
2143 process_re_X('fax', @_);
2146 use Storable qw(thaw);
2150 my( $method, $job ) = ( shift, shift );
2152 my $param = thaw(decode_base64(shift));
2153 warn Dumper($param) if $DEBUG;
2164 my($method, $job, %param ) = @_;
2165 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2167 #some false laziness w/search/cust_bill.html
2169 my $orderby = 'ORDER BY cust_bill._date';
2173 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2174 push @where, "cust_bill._date >= $1";
2176 if ( $param{'end'} =~ /^(\d+)$/ ) {
2177 push @where, "cust_bill._date < $1";
2179 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2180 push @where, "cust_main.agentnum = $1";
2184 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2185 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2186 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2187 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2189 push @where, "0 != $owed"
2192 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2195 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2197 my $addl_from = 'left join cust_main using ( custnum )';
2199 if ( $param{'newest_percust'} ) {
2200 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2201 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2202 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2205 my @cust_bill = qsearch( 'cust_bill',
2207 "$distinct cust_bill.*",
2213 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2214 foreach my $cust_bill ( @cust_bill ) {
2215 $cust_bill->$method();
2217 if ( $job ) { #progressbar foo
2219 if ( time - $min_sec > $last ) {
2220 my $error = $job->update_statustext(
2221 int( 100 * $num / scalar(@cust_bill) )
2223 die $error if $error;
2238 print_text formatting (and some logic :/) is in source, but needs to be
2239 slurped in from a file. Also number of lines ($=).
2243 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2244 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base