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 ( [ -e "$path/logo_". $args{'_template'}. ".png" ] ) {
421 $file = "$path/logo_". $args{'_template'}. ".png";
423 $file = "$path/logo.png";
426 my $image = build MIME::Entity
427 'Type' => 'image/png',
428 'Encoding' => 'base64',
430 'Filename' => 'logo.png',
431 'Content-ID' => "<$content_id>",
434 $alternative->attach(
435 'Type' => 'text/html',
436 'Encoding' => 'quoted-printable',
437 'Data' => [ '<html>',
440 ' '. encode_entities($return{'subject'}),
443 ' <body bgcolor="#e8e8e8">',
444 $self->print_html('', $args{'template'}, $content_id),
448 'Disposition' => 'inline',
449 #'Filename' => 'invoice.pdf',
452 if ( $conf->exists('invoice_email_pdf') ) {
457 # multipart/alternative
463 my $related = build MIME::Entity 'Type' => 'multipart/related',
464 'Encoding' => '7bit';
466 #false laziness w/Misc::send_email
467 $related->head->replace('Content-type',
469 '; boundary="'. $related->head->multipart_boundary. '"'.
470 '; type=multipart/alternative'
473 $related->add_part($alternative);
475 $related->add_part($image);
477 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
479 $return{'mimeparts'} = [ $related, $pdf ];
483 #no other attachment:
485 # multipart/alternative
490 $return{'content-type'} = 'multipart/related';
491 $return{'mimeparts'} = [ $alternative, $image ];
492 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
493 #$return{'disposition'} = 'inline';
499 if ( $conf->exists('invoice_email_pdf') ) {
500 warn "$me creating PDF attachment"
503 #mime parts arguments a la MIME::Entity->build().
504 $return{'mimeparts'} = [
505 { $self->mimebuild_pdf('', $args{'template'}) }
509 if ( $conf->exists('invoice_email_pdf')
510 and scalar($conf->config('invoice_email_pdf_note')) ) {
512 warn "$me using 'invoice_email_pdf_note'"
514 $return{'body'} = [ map { $_ . "\n" }
515 $conf->config('invoice_email_pdf_note')
520 warn "$me not using 'invoice_email_pdf_note'"
522 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
523 $return{'body'} = $args{'print_text'};
525 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
538 Returns a list suitable for passing to MIME::Entity->build(), representing
539 this invoice as PDF attachment.
546 'Type' => 'application/pdf',
547 'Encoding' => 'base64',
548 'Data' => [ $self->print_pdf(@_) ],
549 'Disposition' => 'attachment',
550 'Filename' => 'invoice.pdf',
554 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
556 Sends this invoice to the destinations configured for this customer: send
557 emails or print. See L<FS::cust_main_invoice>.
559 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
561 AGENTNUM, if specified, means that this invoice will only be sent for customers
562 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
563 single agent) or an arrayref of agentnums.
565 INVOICE_FROM, if specified, overrides the default email invoice From: address.
571 my $template = scalar(@_) ? shift : '';
572 if ( scalar(@_) && $_[0] ) {
573 my $agentnums = ref($_[0]) ? shift : [ shift ];
574 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
580 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
582 my @invoicing_list = $self->cust_main->invoicing_list;
584 $self->email($template, $invoice_from)
585 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
587 $self->print($template)
588 if grep { $_ eq 'POST' } @invoicing_list; #postal
590 $self->fax($template)
591 if grep { $_ eq 'FAX' } @invoicing_list; #fax
597 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
601 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
603 INVOICE_FROM, if specified, overrides the default email invoice From: address.
609 my $template = scalar(@_) ? shift : '';
613 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
615 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
616 $self->cust_main->invoicing_list;
618 #better to notify this person than silence
619 @invoicing_list = ($invoice_from) unless @invoicing_list;
621 my $error = send_email(
622 $self->generate_email(
623 'from' => $invoice_from,
624 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
625 'template' => $template,
628 die "can't email invoice: $error\n" if $error;
629 #die "$error\n" if $error;
633 =item lpr_data [ TEMPLATENAME ]
635 Returns the postscript or plaintext for this invoice as an arrayref.
637 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
642 my( $self, $template) = @_;
643 $conf->exists('invoice_latex')
644 ? [ $self->print_ps('', $template) ]
645 : [ $self->print_text('', $template) ];
648 =item print [ TEMPLATENAME ]
652 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
658 my $template = scalar(@_) ? shift : '';
660 my $lpr = $conf->config('lpr');
663 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
665 $outerr = ": $outerr" if length($outerr);
666 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
671 =item fax [ TEMPLATENAME ]
675 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
681 my $template = scalar(@_) ? shift : '';
683 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
684 unless $conf->exists('invoice_latex');
686 my $dialstring = $self->cust_main->getfield('fax');
689 my $error = send_fax( 'docdata' => $self->lpr_data($template),
690 'dialstring' => $dialstring,
692 die $error if $error;
696 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
698 Like B<send>, but only sends the invoice if it is the newest open invoice for
708 grep { $_->owed > 0 }
709 qsearch('cust_bill', {
710 'custnum' => $self->custnum,
711 #'_date' => { op=>'>', value=>$self->_date },
712 'invnum' => { op=>'>', value=>$self->invnum },
719 =item send_csv OPTIONS
721 Sends invoice as a CSV data-file to a remote host with the specified protocol.
725 protocol - currently only "ftp"
731 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
732 and YYMMDDHHMMSS is a timestamp.
734 The fields of the CSV file is as follows:
736 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
740 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
742 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
743 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
744 fields are filled in.
746 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
747 first two fields (B<record_type> and B<invnum>) and the last five fields
748 (B<pkg> through B<edate>) are filled in.
750 =item invnum - invoice number
752 =item custnum - customer number
754 =item _date - invoice date
756 =item charged - total invoice amount
758 =item first - customer first name
760 =item last - customer first name
762 =item company - company name
764 =item address1 - address line 1
766 =item address2 - address line 1
776 =item pkg - line item description
778 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
780 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
782 =item sdate - start date for recurring fee
784 =item edate - end date for recurring fee
791 my($self, %opt) = @_;
793 #part one: create file
795 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
796 mkdir $spooldir, 0700 unless -d $spooldir;
798 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
800 open(CSV, ">$file") or die "can't open $file: $!";
802 eval "use Text::CSV_XS";
805 my $csv = Text::CSV_XS->new({'always_quote'=>1});
807 my $cust_main = $self->cust_main;
813 time2str("%x", $self->_date),
814 sprintf("%.2f", $self->charged),
815 ( map { $cust_main->getfield($_) }
816 qw( first last company address1 address2 city state zip country ) ),
818 ) or die "can't create csv";
819 print CSV $csv->string. "\n";
821 #new charges (false laziness w/print_text)
822 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
824 my($pkg, $setup, $recur, $sdate, $edate);
825 if ( $cust_bill_pkg->pkgnum ) {
827 ($pkg, $setup, $recur, $sdate, $edate) = (
828 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
829 ( $cust_bill_pkg->setup != 0
830 ? sprintf("%.2f", $cust_bill_pkg->setup )
832 ( $cust_bill_pkg->recur != 0
833 ? sprintf("%.2f", $cust_bill_pkg->recur )
835 time2str("%x", $cust_bill_pkg->sdate),
836 time2str("%x", $cust_bill_pkg->edate),
840 next unless $cust_bill_pkg->setup != 0;
841 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
842 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
844 ($pkg, $setup, $recur, $sdate, $edate) =
845 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
851 ( map { '' } (1..11) ),
852 ($pkg, $setup, $recur, $sdate, $edate)
853 ) or die "can't create csv";
854 print CSV $csv->string. "\n";
858 close CSV or die "can't close CSV: $!";
863 if ( $opt{protocol} eq 'ftp' ) {
864 eval "use Net::FTP;";
866 $net = Net::FTP->new($opt{server}) or die @$;
868 die "unknown protocol: $opt{protocol}";
871 $net->login( $opt{username}, $opt{password} )
872 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
874 $net->binary or die "can't set binary mode";
876 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
878 $net->put($file) or die "can't put $file: $!";
888 Pays this invoice with a compliemntary payment. If there is an error,
889 returns the error, otherwise returns false.
895 my $cust_pay = new FS::cust_pay ( {
896 'invnum' => $self->invnum,
897 'paid' => $self->owed,
900 'payinfo' => $self->cust_main->payinfo,
908 Attempts to pay this invoice with a credit card payment via a
909 Business::OnlinePayment realtime gateway. See
910 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
911 for supported processors.
917 $self->realtime_bop( 'CC', @_ );
922 Attempts to pay this invoice with an electronic check (ACH) payment via a
923 Business::OnlinePayment realtime gateway. See
924 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
925 for supported processors.
931 $self->realtime_bop( 'ECHECK', @_ );
936 Attempts to pay this invoice with phone bill (LEC) payment via a
937 Business::OnlinePayment realtime gateway. See
938 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
939 for supported processors.
945 $self->realtime_bop( 'LEC', @_ );
949 my( $self, $method ) = @_;
951 my $cust_main = $self->cust_main;
952 my $balance = $cust_main->balance;
953 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
954 $amount = sprintf("%.2f", $amount);
955 return "not run (balance $balance)" unless $amount > 0;
957 my $description = 'Internet Services';
958 if ( $conf->exists('business-onlinepayment-description') ) {
959 my $dtempl = $conf->config('business-onlinepayment-description');
961 my $agent_obj = $cust_main->agent
962 or die "can't retreive agent for $cust_main (agentnum ".
963 $cust_main->agentnum. ")";
964 my $agent = $agent_obj->agent;
965 my $pkgs = join(', ',
966 map { $_->cust_pkg->part_pkg->pkg }
967 grep { $_->pkgnum } $self->cust_bill_pkg
969 $description = eval qq("$dtempl");
972 $cust_main->realtime_bop($method, $amount,
973 'description' => $description,
974 'invnum' => $self->invnum,
981 Adds a payment for this invoice to the pending credit card batch (see
982 L<FS::cust_pay_batch>).
988 my $cust_main = $self->cust_main;
990 my $cust_pay_batch = new FS::cust_pay_batch ( {
991 'invnum' => $self->getfield('invnum'),
992 'custnum' => $cust_main->getfield('custnum'),
993 'last' => $cust_main->getfield('last'),
994 'first' => $cust_main->getfield('first'),
995 'address1' => $cust_main->getfield('address1'),
996 'address2' => $cust_main->getfield('address2'),
997 'city' => $cust_main->getfield('city'),
998 'state' => $cust_main->getfield('state'),
999 'zip' => $cust_main->getfield('zip'),
1000 'country' => $cust_main->getfield('country'),
1001 'cardnum' => $cust_main->payinfo,
1002 'exp' => $cust_main->getfield('paydate'),
1003 'payname' => $cust_main->getfield('payname'),
1004 'amount' => $self->owed,
1006 my $error = $cust_pay_batch->insert;
1007 die $error if $error;
1012 sub _agent_template {
1014 $self->_agent_plandata('agent_templatename');
1017 sub _agent_invoice_from {
1019 $self->_agent_plandata('agent_invoice_from');
1022 sub _agent_plandata {
1023 my( $self, $option ) = @_;
1025 my $part_bill_event = qsearchs( 'part_bill_event',
1027 'payby' => $self->cust_main->payby,
1028 'plan' => 'send_agent',
1029 'plandata' => { 'op' => '~',
1030 'value' => "(^|\n)agentnum ".
1032 $self->cust_main->agentnum.
1038 'ORDER BY seconds LIMIT 1'
1041 return '' unless $part_bill_event;
1043 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1046 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1047 " plandata for $option";
1053 =item print_text [ TIME [ , TEMPLATE ] ]
1055 Returns an text invoice, as a list of lines.
1057 TIME an optional value used to control the printing of overdue messages. The
1058 default is now. It isn't the date of the invoice; that's the `_date' field.
1059 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1060 L<Time::Local> and L<Date::Parse> for conversion functions.
1064 #still some false laziness w/print_text
1067 my( $self, $today, $template ) = @_;
1070 # my $invnum = $self->invnum;
1071 my $cust_main = $self->cust_main;
1072 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1073 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1075 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1076 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1077 #my $balance_due = $self->owed + $pr_total - $cr_total;
1078 my $balance_due = $self->owed + $pr_total;
1081 #my($description,$amount);
1085 foreach ( @pr_cust_bill ) {
1087 "Previous Balance, Invoice #". $_->invnum.
1088 " (". time2str("%x",$_->_date). ")",
1089 $money_char. sprintf("%10.2f",$_->owed)
1092 if (@pr_cust_bill) {
1093 push @buf,['','-----------'];
1094 push @buf,[ 'Total Previous Balance',
1095 $money_char. sprintf("%10.2f",$pr_total ) ];
1100 foreach my $cust_bill_pkg (
1101 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1102 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1105 if ( $cust_bill_pkg->pkgnum > 0 ) {
1107 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1108 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1109 my $pkg = $part_pkg->pkg;
1111 if ( $cust_bill_pkg->setup != 0 ) {
1112 my $description = $pkg;
1113 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1114 push @buf, [ $description,
1115 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1117 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1118 $cust_pkg->h_labels($self->_date);
1121 if ( $cust_bill_pkg->recur != 0 ) {
1123 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1124 time2str("%x", $cust_bill_pkg->edate) . ")",
1125 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1128 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1129 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1132 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1134 } else { #pkgnum tax or one-shot line item
1135 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1136 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1138 if ( $cust_bill_pkg->setup != 0 ) {
1139 push @buf, [ $itemdesc,
1140 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1142 if ( $cust_bill_pkg->recur != 0 ) {
1143 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1144 . time2str("%x", $cust_bill_pkg->edate). ")",
1145 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1151 push @buf,['','-----------'];
1152 push @buf,['Total New Charges',
1153 $money_char. sprintf("%10.2f",$self->charged) ];
1156 push @buf,['','-----------'];
1157 push @buf,['Total Charges',
1158 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1162 foreach ( $self->cust_credited ) {
1164 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1166 my $reason = substr($_->cust_credit->reason,0,32);
1167 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1168 $reason = " ($reason) " if $reason;
1170 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1172 $money_char. sprintf("%10.2f",$_->amount)
1175 #foreach ( @cr_cust_credit ) {
1177 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1178 # $money_char. sprintf("%10.2f",$_->credited)
1182 #get & print payments
1183 foreach ( $self->cust_bill_pay ) {
1185 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1188 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1189 $money_char. sprintf("%10.2f",$_->amount )
1194 my $balance_due_msg = $self->balance_due_msg;
1196 push @buf,['','-----------'];
1197 push @buf,[$balance_due_msg, $money_char.
1198 sprintf("%10.2f", $balance_due ) ];
1200 #create the template
1201 $template ||= $self->_agent_template;
1202 my $templatefile = 'invoice_template';
1203 $templatefile .= "_$template" if length($template);
1204 my @invoice_template = $conf->config($templatefile)
1205 or die "cannot load config file $templatefile";
1208 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1209 /invoice_lines\((\d*)\)/;
1210 $invoice_lines += $1 || scalar(@buf);
1213 die "no invoice_lines() functions in template?" unless $wasfunc;
1214 my $invoice_template = new Text::Template (
1216 SOURCE => [ map "$_\n", @invoice_template ],
1217 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1218 $invoice_template->compile()
1219 or die "can't compile template: $Text::Template::ERROR";
1221 #setup template variables
1222 package FS::cust_bill::_template; #!
1223 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1225 $invnum = $self->invnum;
1226 $date = $self->_date;
1228 $agent = $self->cust_main->agent->agent;
1230 if ( $FS::cust_bill::invoice_lines ) {
1232 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1234 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1239 #format address (variable for the template)
1241 @address = ( '', '', '', '', '', '' );
1242 package FS::cust_bill; #!
1243 $FS::cust_bill::_template::address[$l++] =
1244 $cust_main->payname.
1245 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1246 ? " (P.O. #". $cust_main->payinfo. ")"
1250 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1251 if $cust_main->company;
1252 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1253 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1254 if $cust_main->address2;
1255 $FS::cust_bill::_template::address[$l++] =
1256 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1258 my $countrydefault = $conf->config('countrydefault') || 'US';
1259 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1260 unless $cust_main->country eq $countrydefault;
1262 # #overdue? (variable for the template)
1263 # $FS::cust_bill::_template::overdue = (
1265 # && $today > $self->_date
1266 ## && $self->printed > 1
1267 # && $self->printed > 0
1270 #and subroutine for the template
1271 sub FS::cust_bill::_template::invoice_lines {
1272 my $lines = shift || scalar(@buf);
1274 scalar(@buf) ? shift @buf : [ '', '' ];
1280 $FS::cust_bill::_template::page = 1;
1284 push @collect, split("\n",
1285 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1287 $FS::cust_bill::_template::page++;
1290 map "$_\n", @collect;
1294 =item print_latex [ TIME [ , TEMPLATE ] ]
1296 Internal method - returns a filename of a filled-in LaTeX template for this
1297 invoice (Note: add ".tex" to get the actual filename).
1299 See print_ps and print_pdf for methods that return PostScript and PDF output.
1301 TIME an optional value used to control the printing of overdue messages. The
1302 default is now. It isn't the date of the invoice; that's the `_date' field.
1303 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1304 L<Time::Local> and L<Date::Parse> for conversion functions.
1308 #still some false laziness w/print_text
1311 my( $self, $today, $template ) = @_;
1313 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1316 my $cust_main = $self->cust_main;
1317 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1318 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1320 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1321 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1322 #my $balance_due = $self->owed + $pr_total - $cr_total;
1323 my $balance_due = $self->owed + $pr_total;
1325 #create the template
1326 $template ||= $self->_agent_template;
1327 my $templatefile = 'invoice_latex';
1328 my $suffix = length($template) ? "_$template" : '';
1329 $templatefile .= $suffix;
1330 my @invoice_template = map "$_\n", $conf->config($templatefile)
1331 or die "cannot load config file $templatefile";
1333 my($format, $text_template);
1334 if ( grep { /^%%Detail/ } @invoice_template ) {
1335 #change this to a die when the old code is removed
1336 warn "old-style invoice template $templatefile; ".
1337 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1340 $format = 'Text::Template';
1341 $text_template = new Text::Template(
1343 SOURCE => \@invoice_template,
1344 DELIMITERS => [ '[@--', '--@]' ],
1347 $text_template->compile()
1348 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1352 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1353 $returnaddress = join("\n",
1354 $conf->config_orbase('invoice_latexreturnaddress', $template)
1357 $returnaddress = '~';
1360 my %invoice_data = (
1361 'invnum' => $self->invnum,
1362 'date' => time2str('%b %o, %Y', $self->_date),
1363 'today' => time2str('%b %o, %Y', $today),
1364 'agent' => _latex_escape($cust_main->agent->agent),
1365 'payname' => _latex_escape($cust_main->payname),
1366 'company' => _latex_escape($cust_main->company),
1367 'address1' => _latex_escape($cust_main->address1),
1368 'address2' => _latex_escape($cust_main->address2),
1369 'city' => _latex_escape($cust_main->city),
1370 'state' => _latex_escape($cust_main->state),
1371 'zip' => _latex_escape($cust_main->zip),
1372 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1373 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1374 'returnaddress' => $returnaddress,
1376 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1377 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1378 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1381 my $countrydefault = $conf->config('countrydefault') || 'US';
1382 if ( $cust_main->country eq $countrydefault ) {
1383 $invoice_data{'country'} = '';
1385 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1388 $invoice_data{'notes'} =
1390 # #do variable substitutions in notes
1391 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1392 $conf->config_orbase('invoice_latexnotes', $template)
1394 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1397 $invoice_data{'footer'} =~ s/\n+$//;
1398 $invoice_data{'smallfooter'} =~ s/\n+$//;
1399 $invoice_data{'notes'} =~ s/\n+$//;
1401 $invoice_data{'po_line'} =
1402 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1403 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1407 if ( $format eq 'old' ) {
1410 my @total_item = ();
1411 while ( @invoice_template ) {
1412 my $line = shift @invoice_template;
1414 if ( $line =~ /^%%Detail\s*$/ ) {
1416 while ( ( my $line_item_line = shift @invoice_template )
1417 !~ /^%%EndDetail\s*$/ ) {
1418 push @line_item, $line_item_line;
1420 foreach my $line_item ( $self->_items ) {
1421 #foreach my $line_item ( $self->_items_pkg ) {
1422 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1423 $invoice_data{'description'} =
1424 _latex_escape($line_item->{'description'});
1425 if ( exists $line_item->{'ext_description'} ) {
1426 $invoice_data{'description'} .=
1427 "\\tabularnewline\n~~".
1428 join( "\\tabularnewline\n~~",
1429 map _latex_escape($_), @{$line_item->{'ext_description'}}
1432 $invoice_data{'amount'} = $line_item->{'amount'};
1433 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1435 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1438 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1440 while ( ( my $total_item_line = shift @invoice_template )
1441 !~ /^%%EndTotalDetails\s*$/ ) {
1442 push @total_item, $total_item_line;
1445 my @total_fill = ();
1448 foreach my $tax ( $self->_items_tax ) {
1449 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1450 $taxtotal += $tax->{'amount'};
1451 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1453 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1458 $invoice_data{'total_item'} = 'Sub-total';
1459 $invoice_data{'total_amount'} =
1460 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1461 unshift @total_fill,
1462 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1466 $invoice_data{'total_item'} = '\textbf{Total}';
1467 $invoice_data{'total_amount'} =
1468 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1470 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1473 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1476 foreach my $credit ( $self->_items_credits ) {
1477 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1479 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1481 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1486 foreach my $payment ( $self->_items_payments ) {
1487 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1489 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1491 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1495 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1496 $invoice_data{'total_amount'} =
1497 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1499 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1502 push @filled_in, @total_fill;
1505 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1506 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1507 push @filled_in, $line;
1518 } elsif ( $format eq 'Text::Template' ) {
1520 my @detail_items = ();
1521 my @total_items = ();
1523 $invoice_data{'detail_items'} = \@detail_items;
1524 $invoice_data{'total_items'} = \@total_items;
1526 foreach my $line_item ( $self->_items ) {
1528 ext_description => [],
1530 $detail->{'ref'} = $line_item->{'pkgnum'};
1531 $detail->{'quantity'} = 1;
1532 $detail->{'description'} = _latex_escape($line_item->{'description'});
1533 if ( exists $line_item->{'ext_description'} ) {
1534 @{$detail->{'ext_description'}} = map {
1536 } @{$line_item->{'ext_description'}};
1538 $detail->{'amount'} = $line_item->{'amount'};
1539 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1541 push @detail_items, $detail;
1546 foreach my $tax ( $self->_items_tax ) {
1548 $total->{'total_item'} = _latex_escape($tax->{'description'});
1549 $taxtotal += $tax->{'amount'};
1550 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1551 push @total_items, $total;
1556 $total->{'total_item'} = 'Sub-total';
1557 $total->{'total_amount'} =
1558 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1559 unshift @total_items, $total;
1564 $total->{'total_item'} = '\textbf{Total}';
1565 $total->{'total_amount'} =
1566 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1567 push @total_items, $total;
1570 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1573 foreach my $credit ( $self->_items_credits ) {
1575 $total->{'total_item'} = _latex_escape($credit->{'description'});
1577 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1578 push @total_items, $total;
1582 foreach my $payment ( $self->_items_payments ) {
1584 $total->{'total_item'} = _latex_escape($payment->{'description'});
1586 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1587 push @total_items, $total;
1592 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1593 $total->{'total_amount'} =
1594 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1595 push @total_items, $total;
1599 die "guru meditation #54";
1602 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1603 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1607 ) or die "can't open temp file: $!\n";
1608 if ( $format eq 'old' ) {
1609 print $fh join('', @filled_in );
1610 } elsif ( $format eq 'Text::Template' ) {
1611 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1613 die "guru meditation #32";
1617 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1622 =item print_ps [ TIME [ , TEMPLATE ] ]
1624 Returns an postscript invoice, as a scalar.
1626 TIME an optional value used to control the printing of overdue messages. The
1627 default is now. It isn't the date of the invoice; that's the `_date' field.
1628 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1629 L<Time::Local> and L<Date::Parse> for conversion functions.
1636 my $file = $self->print_latex(@_);
1638 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1641 my $sfile = shell_quote $file;
1643 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1644 or die "pslatex $file.tex failed; see $file.log for details?\n";
1645 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1646 or die "pslatex $file.tex failed; see $file.log for details?\n";
1648 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1649 or die "dvips failed";
1651 open(POSTSCRIPT, "<$file.ps")
1652 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1654 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1657 while (<POSTSCRIPT>) {
1667 =item print_pdf [ TIME [ , TEMPLATE ] ]
1669 Returns an PDF invoice, as a scalar.
1671 TIME an optional value used to control the printing of overdue messages. The
1672 default is now. It isn't the date of the invoice; that's the `_date' field.
1673 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1674 L<Time::Local> and L<Date::Parse> for conversion functions.
1681 my $file = $self->print_latex(@_);
1683 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1686 #system('pdflatex', "$file.tex");
1687 #system('pdflatex', "$file.tex");
1688 #! LaTeX Error: Unknown graphics extension: .eps.
1690 my $sfile = shell_quote $file;
1692 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1693 or die "pslatex $file.tex failed; see $file.log for details?\n";
1694 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1695 or die "pslatex $file.tex failed; see $file.log for details?\n";
1697 #system('dvipdf', "$file.dvi", "$file.pdf" );
1699 "dvips -q -t letter -f $sfile.dvi ".
1700 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1703 or die "dvips | gs failed: $!";
1705 open(PDF, "<$file.pdf")
1706 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1708 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1721 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1723 Returns an HTML invoice, as a scalar.
1725 TIME an optional value used to control the printing of overdue messages. The
1726 default is now. It isn't the date of the invoice; that's the `_date' field.
1727 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1728 L<Time::Local> and L<Date::Parse> for conversion functions.
1730 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1731 when emailing the invoice as part of a multipart/related MIME email.
1736 my( $self, $today, $template, $cid ) = @_;
1739 my $cust_main = $self->cust_main;
1740 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1741 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1743 $template ||= $self->_agent_template;
1744 my $templatefile = 'invoice_html';
1745 my $suffix = length($template) ? "_$template" : '';
1746 $templatefile .= $suffix;
1747 my @html_template = map "$_\n", $conf->config($templatefile)
1748 or die "cannot load config file $templatefile";
1750 my $html_template = new Text::Template(
1752 SOURCE => \@html_template,
1753 DELIMITERS => [ '<%=', '%>' ],
1756 $html_template->compile()
1757 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1759 my %invoice_data = (
1760 'invnum' => $self->invnum,
1761 'date' => time2str('%b %o, %Y', $self->_date),
1762 'today' => time2str('%b %o, %Y', $today),
1763 'agent' => encode_entities($cust_main->agent->agent),
1764 'payname' => encode_entities($cust_main->payname),
1765 'company' => encode_entities($cust_main->company),
1766 'address1' => encode_entities($cust_main->address1),
1767 'address2' => encode_entities($cust_main->address2),
1768 'city' => encode_entities($cust_main->city),
1769 'state' => encode_entities($cust_main->state),
1770 'zip' => encode_entities($cust_main->zip),
1771 'terms' => $conf->config('invoice_default_terms')
1772 || 'Payable upon receipt',
1774 'template' => $template,
1775 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1778 $invoice_data{'returnaddress'} =
1779 length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1780 ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1783 s/\\\\\*?\s*$/<BR>/;
1784 s/\\hyphenation\{[\w\s\-]+\}//;
1787 $conf->config_orbase('invoice_latexreturnaddress', $template)
1790 my $countrydefault = $conf->config('countrydefault') || 'US';
1791 if ( $cust_main->country eq $countrydefault ) {
1792 $invoice_data{'country'} = '';
1794 $invoice_data{'country'} =
1795 encode_entities(code2country($cust_main->country));
1798 $invoice_data{'notes'} =
1799 length($conf->config_orbase('invoice_htmlnotes', $template))
1800 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1802 s/%%(.*)$/<!-- $1 -->/;
1803 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1804 s/\\begin\{enumerate\}/<ol>/;
1806 s/\\end\{enumerate\}/<\/ol>/;
1807 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1810 $conf->config_orbase('invoice_latexnotes', $template)
1813 # #do variable substitutions in notes
1814 # $invoice_data{'notes'} =
1816 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1817 # $conf->config_orbase('invoice_latexnotes', $suffix)
1820 $invoice_data{'footer'} =
1821 length($conf->config_orbase('invoice_htmlfooter', $template))
1822 ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) )
1823 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1824 $conf->config_orbase('invoice_latexfooter', $template)
1827 $invoice_data{'po_line'} =
1828 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1829 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1832 my $money_char = $conf->config('money_char') || '$';
1834 foreach my $line_item ( $self->_items ) {
1836 ext_description => [],
1838 $detail->{'ref'} = $line_item->{'pkgnum'};
1839 $detail->{'description'} = encode_entities($line_item->{'description'});
1840 if ( exists $line_item->{'ext_description'} ) {
1841 @{$detail->{'ext_description'}} = map {
1842 encode_entities($_);
1843 } @{$line_item->{'ext_description'}};
1845 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1846 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1848 push @{$invoice_data{'detail_items'}}, $detail;
1853 foreach my $tax ( $self->_items_tax ) {
1855 $total->{'total_item'} = encode_entities($tax->{'description'});
1856 $taxtotal += $tax->{'amount'};
1857 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1858 push @{$invoice_data{'total_items'}}, $total;
1863 $total->{'total_item'} = 'Sub-total';
1864 $total->{'total_amount'} =
1865 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1866 unshift @{$invoice_data{'total_items'}}, $total;
1869 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1872 $total->{'total_item'} = '<b>Total</b>';
1873 $total->{'total_amount'} =
1874 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1875 push @{$invoice_data{'total_items'}}, $total;
1878 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1881 foreach my $credit ( $self->_items_credits ) {
1883 $total->{'total_item'} = encode_entities($credit->{'description'});
1885 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1886 push @{$invoice_data{'total_items'}}, $total;
1890 foreach my $payment ( $self->_items_payments ) {
1892 $total->{'total_item'} = encode_entities($payment->{'description'});
1894 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1895 push @{$invoice_data{'total_items'}}, $total;
1900 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1901 $total->{'total_amount'} =
1902 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1903 push @{$invoice_data{'total_items'}}, $total;
1906 $html_template->fill_in( HASH => \%invoice_data);
1909 # quick subroutine for print_latex
1911 # There are ten characters that LaTeX treats as special characters, which
1912 # means that they do not simply typeset themselves:
1913 # # $ % & ~ _ ^ \ { }
1915 # TeX ignores blanks following an escaped character; if you want a blank (as
1916 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1920 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1921 $value =~ s/([<>])/\$$1\$/g;
1925 #utility methods for print_*
1927 sub balance_due_msg {
1929 my $msg = 'Balance Due';
1930 return $msg unless $conf->exists('invoice_default_terms');
1931 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1932 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1933 } elsif ( $conf->config('invoice_default_terms') ) {
1934 $msg .= ' - '. $conf->config('invoice_default_terms');
1941 my @display = scalar(@_)
1943 : qw( _items_previous _items_pkg );
1944 #: qw( _items_pkg );
1945 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1947 foreach my $display ( @display ) {
1948 push @b, $self->$display(@_);
1953 sub _items_previous {
1955 my $cust_main = $self->cust_main;
1956 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1958 foreach ( @pr_cust_bill ) {
1960 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1961 ' ('. time2str('%x',$_->_date). ')',
1962 #'pkgpart' => 'N/A',
1964 'amount' => sprintf("%.2f", $_->owed),
1970 # 'description' => 'Previous Balance',
1971 # #'pkgpart' => 'N/A',
1972 # 'pkgnum' => 'N/A',
1973 # 'amount' => sprintf("%10.2f", $pr_total ),
1974 # 'ext_description' => [ map {
1975 # "Invoice ". $_->invnum.
1976 # " (". time2str("%x",$_->_date). ") ".
1977 # sprintf("%10.2f", $_->owed)
1978 # } @pr_cust_bill ],
1985 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1986 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1991 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1992 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1995 sub _items_cust_bill_pkg {
1997 my $cust_bill_pkg = shift;
2000 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2002 if ( $cust_bill_pkg->pkgnum > 0 ) {
2004 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
2005 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
2006 my $pkg = $part_pkg->pkg;
2008 if ( $cust_bill_pkg->setup != 0 ) {
2009 my $description = $pkg;
2010 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2011 my @d = $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_pkg->pkgnum,
2017 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2018 ext_description => \@d,
2022 if ( $cust_bill_pkg->recur != 0 ) {
2024 description => "$pkg (" .
2025 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2026 time2str('%x', $cust_bill_pkg->edate). ')',
2027 #pkgpart => $part_pkg->pkgpart,
2028 pkgnum => $cust_pkg->pkgnum,
2029 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2030 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2031 $cust_bill_pkg->sdate),
2032 $cust_bill_pkg->details,
2037 } else { #pkgnum tax or one-shot line item (??)
2039 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2040 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2042 if ( $cust_bill_pkg->setup != 0 ) {
2044 'description' => $itemdesc,
2045 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2048 if ( $cust_bill_pkg->recur != 0 ) {
2050 'description' => "$itemdesc (".
2051 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2052 time2str("%x", $cust_bill_pkg->edate). ')',
2053 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2065 sub _items_credits {
2070 foreach ( $self->cust_credited ) {
2072 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2074 my $reason = $_->cust_credit->reason;
2075 #my $reason = substr($_->cust_credit->reason,0,32);
2076 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2077 $reason = " ($reason) " if $reason;
2079 #'description' => 'Credit ref\#'. $_->crednum.
2080 # " (". time2str("%x",$_->cust_credit->_date) .")".
2082 'description' => 'Credit applied '.
2083 time2str("%x",$_->cust_credit->_date). $reason,
2084 'amount' => sprintf("%.2f",$_->amount),
2087 #foreach ( @cr_cust_credit ) {
2089 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2090 # $money_char. sprintf("%10.2f",$_->credited)
2098 sub _items_payments {
2102 #get & print payments
2103 foreach ( $self->cust_bill_pay ) {
2105 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2108 'description' => "Payment received ".
2109 time2str("%x",$_->cust_pay->_date ),
2110 'amount' => sprintf("%.2f", $_->amount )
2128 sub process_reprint {
2129 process_re_X('print', @_);
2136 sub process_reemail {
2137 process_re_X('email', @_);
2145 process_re_X('fax', @_);
2148 use Storable qw(thaw);
2152 my( $method, $job ) = ( shift, shift );
2154 my $param = thaw(decode_base64(shift));
2155 warn Dumper($param) if $DEBUG;
2166 my($method, $job, %param ) = @_;
2167 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2169 #some false laziness w/search/cust_bill.html
2171 my $orderby = 'ORDER BY cust_bill._date';
2175 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2176 push @where, "cust_bill._date >= $1";
2178 if ( $param{'end'} =~ /^(\d+)$/ ) {
2179 push @where, "cust_bill._date < $1";
2181 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2182 push @where, "cust_main.agentnum = $1";
2186 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2187 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2188 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2189 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2191 push @where, "0 != $owed"
2194 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2197 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2199 my $addl_from = 'left join cust_main using ( custnum )';
2201 if ( $param{'newest_percust'} ) {
2202 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2203 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2204 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2207 my @cust_bill = qsearch( 'cust_bill',
2209 "$distinct cust_bill.*",
2215 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2216 foreach my $cust_bill ( @cust_bill ) {
2217 $cust_bill->$method();
2219 if ( $job ) { #progressbar foo
2221 if ( time - $min_sec > $last ) {
2222 my $error = $job->update_statustext(
2223 int( 100 * $num / scalar(@cust_bill) )
2225 die $error if $error;
2240 print_text formatting (and some logic :/) is in source, but needs to be
2241 slurped in from a file. Also number of lines ($=).
2245 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2246 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base