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 and _items stuff)
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/_items stuff (and send_csv)
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 my $desc = $cust_bill_pkg->desc;
1107 if ( $cust_bill_pkg->pkgnum > 0 ) {
1109 if ( $cust_bill_pkg->setup != 0 ) {
1110 my $description = $desc;
1111 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1112 push @buf, [ $description,
1113 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1115 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1116 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1119 if ( $cust_bill_pkg->recur != 0 ) {
1121 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1122 time2str("%x", $cust_bill_pkg->edate) . ")",
1123 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1126 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1127 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1128 $cust_bill_pkg->sdate );
1131 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1133 } else { #pkgnum tax or one-shot line item
1135 if ( $cust_bill_pkg->setup != 0 ) {
1137 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1139 if ( $cust_bill_pkg->recur != 0 ) {
1140 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1141 . time2str("%x", $cust_bill_pkg->edate). ")",
1142 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1150 push @buf,['','-----------'];
1151 push @buf,['Total New Charges',
1152 $money_char. sprintf("%10.2f",$self->charged) ];
1155 push @buf,['','-----------'];
1156 push @buf,['Total Charges',
1157 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1161 foreach ( $self->cust_credited ) {
1163 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1165 my $reason = substr($_->cust_credit->reason,0,32);
1166 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1167 $reason = " ($reason) " if $reason;
1169 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1171 $money_char. sprintf("%10.2f",$_->amount)
1174 #foreach ( @cr_cust_credit ) {
1176 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1177 # $money_char. sprintf("%10.2f",$_->credited)
1181 #get & print payments
1182 foreach ( $self->cust_bill_pay ) {
1184 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1187 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1188 $money_char. sprintf("%10.2f",$_->amount )
1193 my $balance_due_msg = $self->balance_due_msg;
1195 push @buf,['','-----------'];
1196 push @buf,[$balance_due_msg, $money_char.
1197 sprintf("%10.2f", $balance_due ) ];
1199 #create the template
1200 $template ||= $self->_agent_template;
1201 my $templatefile = 'invoice_template';
1202 $templatefile .= "_$template" if length($template);
1203 my @invoice_template = $conf->config($templatefile)
1204 or die "cannot load config file $templatefile";
1207 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1208 /invoice_lines\((\d*)\)/;
1209 $invoice_lines += $1 || scalar(@buf);
1212 die "no invoice_lines() functions in template?" unless $wasfunc;
1213 my $invoice_template = new Text::Template (
1215 SOURCE => [ map "$_\n", @invoice_template ],
1216 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1217 $invoice_template->compile()
1218 or die "can't compile template: $Text::Template::ERROR";
1220 #setup template variables
1221 package FS::cust_bill::_template; #!
1222 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1224 $invnum = $self->invnum;
1225 $date = $self->_date;
1227 $agent = $self->cust_main->agent->agent;
1229 if ( $FS::cust_bill::invoice_lines ) {
1231 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1233 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1238 #format address (variable for the template)
1240 @address = ( '', '', '', '', '', '' );
1241 package FS::cust_bill; #!
1242 $FS::cust_bill::_template::address[$l++] =
1243 $cust_main->payname.
1244 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1245 ? " (P.O. #". $cust_main->payinfo. ")"
1249 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1250 if $cust_main->company;
1251 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1252 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1253 if $cust_main->address2;
1254 $FS::cust_bill::_template::address[$l++] =
1255 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1257 my $countrydefault = $conf->config('countrydefault') || 'US';
1258 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1259 unless $cust_main->country eq $countrydefault;
1261 # #overdue? (variable for the template)
1262 # $FS::cust_bill::_template::overdue = (
1264 # && $today > $self->_date
1265 ## && $self->printed > 1
1266 # && $self->printed > 0
1269 #and subroutine for the template
1270 sub FS::cust_bill::_template::invoice_lines {
1271 my $lines = shift || scalar(@buf);
1273 scalar(@buf) ? shift @buf : [ '', '' ];
1279 $FS::cust_bill::_template::page = 1;
1283 push @collect, split("\n",
1284 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1286 $FS::cust_bill::_template::page++;
1289 map "$_\n", @collect;
1293 =item print_latex [ TIME [ , TEMPLATE ] ]
1295 Internal method - returns a filename of a filled-in LaTeX template for this
1296 invoice (Note: add ".tex" to get the actual filename).
1298 See print_ps and print_pdf for methods that return PostScript and PDF output.
1300 TIME an optional value used to control the printing of overdue messages. The
1301 default is now. It isn't the date of the invoice; that's the `_date' field.
1302 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1303 L<Time::Local> and L<Date::Parse> for conversion functions.
1307 #still some false laziness w/print_text (mostly print_text should use _items stuff though)
1310 my( $self, $today, $template ) = @_;
1312 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1315 my $cust_main = $self->cust_main;
1316 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1317 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1319 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1320 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1321 #my $balance_due = $self->owed + $pr_total - $cr_total;
1322 my $balance_due = $self->owed + $pr_total;
1324 #create the template
1325 $template ||= $self->_agent_template;
1326 my $templatefile = 'invoice_latex';
1327 my $suffix = length($template) ? "_$template" : '';
1328 $templatefile .= $suffix;
1329 my @invoice_template = map "$_\n", $conf->config($templatefile)
1330 or die "cannot load config file $templatefile";
1332 my($format, $text_template);
1333 if ( grep { /^%%Detail/ } @invoice_template ) {
1334 #change this to a die when the old code is removed
1335 warn "old-style invoice template $templatefile; ".
1336 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1339 $format = 'Text::Template';
1340 $text_template = new Text::Template(
1342 SOURCE => \@invoice_template,
1343 DELIMITERS => [ '[@--', '--@]' ],
1346 $text_template->compile()
1347 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1351 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1352 $returnaddress = join("\n",
1353 $conf->config_orbase('invoice_latexreturnaddress', $template)
1356 $returnaddress = '~';
1359 my %invoice_data = (
1360 'invnum' => $self->invnum,
1361 'date' => time2str('%b %o, %Y', $self->_date),
1362 'today' => time2str('%b %o, %Y', $today),
1363 'agent' => _latex_escape($cust_main->agent->agent),
1364 'payname' => _latex_escape($cust_main->payname),
1365 'company' => _latex_escape($cust_main->company),
1366 'address1' => _latex_escape($cust_main->address1),
1367 'address2' => _latex_escape($cust_main->address2),
1368 'city' => _latex_escape($cust_main->city),
1369 'state' => _latex_escape($cust_main->state),
1370 'zip' => _latex_escape($cust_main->zip),
1371 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1372 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1373 'returnaddress' => $returnaddress,
1375 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1376 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1377 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1380 my $countrydefault = $conf->config('countrydefault') || 'US';
1381 if ( $cust_main->country eq $countrydefault ) {
1382 $invoice_data{'country'} = '';
1384 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1387 $invoice_data{'notes'} =
1389 # #do variable substitutions in notes
1390 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1391 $conf->config_orbase('invoice_latexnotes', $template)
1393 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1396 $invoice_data{'footer'} =~ s/\n+$//;
1397 $invoice_data{'smallfooter'} =~ s/\n+$//;
1398 $invoice_data{'notes'} =~ s/\n+$//;
1400 $invoice_data{'po_line'} =
1401 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1402 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1406 if ( $format eq 'old' ) {
1409 my @total_item = ();
1410 while ( @invoice_template ) {
1411 my $line = shift @invoice_template;
1413 if ( $line =~ /^%%Detail\s*$/ ) {
1415 while ( ( my $line_item_line = shift @invoice_template )
1416 !~ /^%%EndDetail\s*$/ ) {
1417 push @line_item, $line_item_line;
1419 foreach my $line_item ( $self->_items ) {
1420 #foreach my $line_item ( $self->_items_pkg ) {
1421 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1422 $invoice_data{'description'} =
1423 _latex_escape($line_item->{'description'});
1424 if ( exists $line_item->{'ext_description'} ) {
1425 $invoice_data{'description'} .=
1426 "\\tabularnewline\n~~".
1427 join( "\\tabularnewline\n~~",
1428 map _latex_escape($_), @{$line_item->{'ext_description'}}
1431 $invoice_data{'amount'} = $line_item->{'amount'};
1432 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1434 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1437 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1439 while ( ( my $total_item_line = shift @invoice_template )
1440 !~ /^%%EndTotalDetails\s*$/ ) {
1441 push @total_item, $total_item_line;
1444 my @total_fill = ();
1447 foreach my $tax ( $self->_items_tax ) {
1448 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1449 $taxtotal += $tax->{'amount'};
1450 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1452 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1457 $invoice_data{'total_item'} = 'Sub-total';
1458 $invoice_data{'total_amount'} =
1459 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1460 unshift @total_fill,
1461 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1465 $invoice_data{'total_item'} = '\textbf{Total}';
1466 $invoice_data{'total_amount'} =
1467 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1469 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1472 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1475 foreach my $credit ( $self->_items_credits ) {
1476 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1478 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1480 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1485 foreach my $payment ( $self->_items_payments ) {
1486 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1488 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1490 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1494 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1495 $invoice_data{'total_amount'} =
1496 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1498 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1501 push @filled_in, @total_fill;
1504 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1505 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1506 push @filled_in, $line;
1517 } elsif ( $format eq 'Text::Template' ) {
1519 my @detail_items = ();
1520 my @total_items = ();
1522 $invoice_data{'detail_items'} = \@detail_items;
1523 $invoice_data{'total_items'} = \@total_items;
1525 foreach my $line_item ( $self->_items ) {
1527 ext_description => [],
1529 $detail->{'ref'} = $line_item->{'pkgnum'};
1530 $detail->{'quantity'} = 1;
1531 $detail->{'description'} = _latex_escape($line_item->{'description'});
1532 if ( exists $line_item->{'ext_description'} ) {
1533 @{$detail->{'ext_description'}} = map {
1535 } @{$line_item->{'ext_description'}};
1537 $detail->{'amount'} = $line_item->{'amount'};
1538 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1540 push @detail_items, $detail;
1545 foreach my $tax ( $self->_items_tax ) {
1547 $total->{'total_item'} = _latex_escape($tax->{'description'});
1548 $taxtotal += $tax->{'amount'};
1549 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1550 push @total_items, $total;
1555 $total->{'total_item'} = 'Sub-total';
1556 $total->{'total_amount'} =
1557 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1558 unshift @total_items, $total;
1563 $total->{'total_item'} = '\textbf{Total}';
1564 $total->{'total_amount'} =
1565 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1566 push @total_items, $total;
1569 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1572 foreach my $credit ( $self->_items_credits ) {
1574 $total->{'total_item'} = _latex_escape($credit->{'description'});
1576 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1577 push @total_items, $total;
1581 foreach my $payment ( $self->_items_payments ) {
1583 $total->{'total_item'} = _latex_escape($payment->{'description'});
1585 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1586 push @total_items, $total;
1591 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1592 $total->{'total_amount'} =
1593 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1594 push @total_items, $total;
1598 die "guru meditation #54";
1601 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1602 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1606 ) or die "can't open temp file: $!\n";
1607 if ( $format eq 'old' ) {
1608 print $fh join('', @filled_in );
1609 } elsif ( $format eq 'Text::Template' ) {
1610 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1612 die "guru meditation #32";
1616 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1621 =item print_ps [ TIME [ , TEMPLATE ] ]
1623 Returns an postscript invoice, as a scalar.
1625 TIME an optional value used to control the printing of overdue messages. The
1626 default is now. It isn't the date of the invoice; that's the `_date' field.
1627 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1628 L<Time::Local> and L<Date::Parse> for conversion functions.
1635 my $file = $self->print_latex(@_);
1637 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1640 my $sfile = shell_quote $file;
1642 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1643 or die "pslatex $file.tex failed; see $file.log for details?\n";
1644 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1645 or die "pslatex $file.tex failed; see $file.log for details?\n";
1647 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1648 or die "dvips failed";
1650 open(POSTSCRIPT, "<$file.ps")
1651 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1653 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1656 while (<POSTSCRIPT>) {
1666 =item print_pdf [ TIME [ , TEMPLATE ] ]
1668 Returns an PDF invoice, as a scalar.
1670 TIME an optional value used to control the printing of overdue messages. The
1671 default is now. It isn't the date of the invoice; that's the `_date' field.
1672 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1673 L<Time::Local> and L<Date::Parse> for conversion functions.
1680 my $file = $self->print_latex(@_);
1682 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1685 #system('pdflatex', "$file.tex");
1686 #system('pdflatex', "$file.tex");
1687 #! LaTeX Error: Unknown graphics extension: .eps.
1689 my $sfile = shell_quote $file;
1691 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1692 or die "pslatex $file.tex failed; see $file.log for details?\n";
1693 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1694 or die "pslatex $file.tex failed; see $file.log for details?\n";
1696 #system('dvipdf', "$file.dvi", "$file.pdf" );
1698 "dvips -q -t letter -f $sfile.dvi ".
1699 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1702 or die "dvips | gs failed: $!";
1704 open(PDF, "<$file.pdf")
1705 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1707 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1720 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1722 Returns an HTML invoice, as a scalar.
1724 TIME an optional value used to control the printing of overdue messages. The
1725 default is now. It isn't the date of the invoice; that's the `_date' field.
1726 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1727 L<Time::Local> and L<Date::Parse> for conversion functions.
1729 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1730 when emailing the invoice as part of a multipart/related MIME email.
1735 my( $self, $today, $template, $cid ) = @_;
1738 my $cust_main = $self->cust_main;
1739 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1740 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1742 $template ||= $self->_agent_template;
1743 my $templatefile = 'invoice_html';
1744 my $suffix = length($template) ? "_$template" : '';
1745 $templatefile .= $suffix;
1746 my @html_template = map "$_\n", $conf->config($templatefile)
1747 or die "cannot load config file $templatefile";
1749 my $html_template = new Text::Template(
1751 SOURCE => \@html_template,
1752 DELIMITERS => [ '<%=', '%>' ],
1755 $html_template->compile()
1756 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1758 my %invoice_data = (
1759 'invnum' => $self->invnum,
1760 'date' => time2str('%b %o, %Y', $self->_date),
1761 'today' => time2str('%b %o, %Y', $today),
1762 'agent' => encode_entities($cust_main->agent->agent),
1763 'payname' => encode_entities($cust_main->payname),
1764 'company' => encode_entities($cust_main->company),
1765 'address1' => encode_entities($cust_main->address1),
1766 'address2' => encode_entities($cust_main->address2),
1767 'city' => encode_entities($cust_main->city),
1768 'state' => encode_entities($cust_main->state),
1769 'zip' => encode_entities($cust_main->zip),
1770 'terms' => $conf->config('invoice_default_terms')
1771 || 'Payable upon receipt',
1773 'template' => $template,
1774 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1777 $invoice_data{'returnaddress'} =
1778 length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1779 ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1782 s/\\\\\*?\s*$/<BR>/;
1783 s/\\hyphenation\{[\w\s\-]+\}//;
1786 $conf->config_orbase('invoice_latexreturnaddress', $template)
1789 my $countrydefault = $conf->config('countrydefault') || 'US';
1790 if ( $cust_main->country eq $countrydefault ) {
1791 $invoice_data{'country'} = '';
1793 $invoice_data{'country'} =
1794 encode_entities(code2country($cust_main->country));
1797 $invoice_data{'notes'} =
1798 length($conf->config_orbase('invoice_htmlnotes', $template))
1799 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1801 s/%%(.*)$/<!-- $1 -->/;
1802 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1803 s/\\begin\{enumerate\}/<ol>/;
1805 s/\\end\{enumerate\}/<\/ol>/;
1806 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1809 $conf->config_orbase('invoice_latexnotes', $template)
1812 # #do variable substitutions in notes
1813 # $invoice_data{'notes'} =
1815 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1816 # $conf->config_orbase('invoice_latexnotes', $suffix)
1819 $invoice_data{'footer'} =
1820 length($conf->config_orbase('invoice_htmlfooter', $template))
1821 ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) )
1822 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1823 $conf->config_orbase('invoice_latexfooter', $template)
1826 $invoice_data{'po_line'} =
1827 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1828 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1831 my $money_char = $conf->config('money_char') || '$';
1833 foreach my $line_item ( $self->_items ) {
1835 ext_description => [],
1837 $detail->{'ref'} = $line_item->{'pkgnum'};
1838 $detail->{'description'} = encode_entities($line_item->{'description'});
1839 if ( exists $line_item->{'ext_description'} ) {
1840 @{$detail->{'ext_description'}} = map {
1841 encode_entities($_);
1842 } @{$line_item->{'ext_description'}};
1844 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1845 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1847 push @{$invoice_data{'detail_items'}}, $detail;
1852 foreach my $tax ( $self->_items_tax ) {
1854 $total->{'total_item'} = encode_entities($tax->{'description'});
1855 $taxtotal += $tax->{'amount'};
1856 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1857 push @{$invoice_data{'total_items'}}, $total;
1862 $total->{'total_item'} = 'Sub-total';
1863 $total->{'total_amount'} =
1864 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1865 unshift @{$invoice_data{'total_items'}}, $total;
1868 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1871 $total->{'total_item'} = '<b>Total</b>';
1872 $total->{'total_amount'} =
1873 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1874 push @{$invoice_data{'total_items'}}, $total;
1877 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1880 foreach my $credit ( $self->_items_credits ) {
1882 $total->{'total_item'} = encode_entities($credit->{'description'});
1884 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1885 push @{$invoice_data{'total_items'}}, $total;
1889 foreach my $payment ( $self->_items_payments ) {
1891 $total->{'total_item'} = encode_entities($payment->{'description'});
1893 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1894 push @{$invoice_data{'total_items'}}, $total;
1899 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1900 $total->{'total_amount'} =
1901 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1902 push @{$invoice_data{'total_items'}}, $total;
1905 $html_template->fill_in( HASH => \%invoice_data);
1908 # quick subroutine for print_latex
1910 # There are ten characters that LaTeX treats as special characters, which
1911 # means that they do not simply typeset themselves:
1912 # # $ % & ~ _ ^ \ { }
1914 # TeX ignores blanks following an escaped character; if you want a blank (as
1915 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1919 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1920 $value =~ s/([<>])/\$$1\$/g;
1924 #utility methods for print_*
1926 sub balance_due_msg {
1928 my $msg = 'Balance Due';
1929 return $msg unless $conf->exists('invoice_default_terms');
1930 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1931 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1932 } elsif ( $conf->config('invoice_default_terms') ) {
1933 $msg .= ' - '. $conf->config('invoice_default_terms');
1940 my @display = scalar(@_)
1942 : qw( _items_previous _items_pkg );
1943 #: qw( _items_pkg );
1944 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1946 foreach my $display ( @display ) {
1947 push @b, $self->$display(@_);
1952 sub _items_previous {
1954 my $cust_main = $self->cust_main;
1955 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1957 foreach ( @pr_cust_bill ) {
1959 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1960 ' ('. time2str('%x',$_->_date). ')',
1961 #'pkgpart' => 'N/A',
1963 'amount' => sprintf("%.2f", $_->owed),
1969 # 'description' => 'Previous Balance',
1970 # #'pkgpart' => 'N/A',
1971 # 'pkgnum' => 'N/A',
1972 # 'amount' => sprintf("%10.2f", $pr_total ),
1973 # 'ext_description' => [ map {
1974 # "Invoice ". $_->invnum.
1975 # " (". time2str("%x",$_->_date). ") ".
1976 # sprintf("%10.2f", $_->owed)
1977 # } @pr_cust_bill ],
1984 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1985 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1990 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1991 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1994 sub _items_cust_bill_pkg {
1996 my $cust_bill_pkg = shift;
1999 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2001 my $desc = $cust_bill_pkg->desc;
2003 if ( $cust_bill_pkg->pkgnum > 0 ) {
2005 if ( $cust_bill_pkg->setup != 0 ) {
2006 my $description = $desc;
2007 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2008 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2009 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2011 description => $description,
2012 #pkgpart => $part_pkg->pkgpart,
2013 pkgnum => $cust_bill_pkg->pkgnum,
2014 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2015 ext_description => \@d,
2019 if ( $cust_bill_pkg->recur != 0 ) {
2021 description => "$desc (" .
2022 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2023 time2str('%x', $cust_bill_pkg->edate). ')',
2024 #pkgpart => $part_pkg->pkgpart,
2025 pkgnum => $cust_bill_pkg->pkgnum,
2026 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2028 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2029 $cust_bill_pkg->sdate),
2030 $cust_bill_pkg->details,
2035 } else { #pkgnum tax or one-shot line item (??)
2037 if ( $cust_bill_pkg->setup != 0 ) {
2039 'description' => $desc,
2040 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2043 if ( $cust_bill_pkg->recur != 0 ) {
2045 'description' => "$desc (".
2046 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2047 time2str("%x", $cust_bill_pkg->edate). ')',
2048 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2060 sub _items_credits {
2065 foreach ( $self->cust_credited ) {
2067 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2069 my $reason = $_->cust_credit->reason;
2070 #my $reason = substr($_->cust_credit->reason,0,32);
2071 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2072 $reason = " ($reason) " if $reason;
2074 #'description' => 'Credit ref\#'. $_->crednum.
2075 # " (". time2str("%x",$_->cust_credit->_date) .")".
2077 'description' => 'Credit applied '.
2078 time2str("%x",$_->cust_credit->_date). $reason,
2079 'amount' => sprintf("%.2f",$_->amount),
2082 #foreach ( @cr_cust_credit ) {
2084 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2085 # $money_char. sprintf("%10.2f",$_->credited)
2093 sub _items_payments {
2097 #get & print payments
2098 foreach ( $self->cust_bill_pay ) {
2100 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2103 'description' => "Payment received ".
2104 time2str("%x",$_->cust_pay->_date ),
2105 'amount' => sprintf("%.2f", $_->amount )
2123 sub process_reprint {
2124 process_re_X('print', @_);
2131 sub process_reemail {
2132 process_re_X('email', @_);
2140 process_re_X('fax', @_);
2143 use Storable qw(thaw);
2147 my( $method, $job ) = ( shift, shift );
2149 my $param = thaw(decode_base64(shift));
2150 warn Dumper($param) if $DEBUG;
2161 my($method, $job, %param ) = @_;
2162 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2164 #some false laziness w/search/cust_bill.html
2166 my $orderby = 'ORDER BY cust_bill._date';
2170 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2171 push @where, "cust_bill._date >= $1";
2173 if ( $param{'end'} =~ /^(\d+)$/ ) {
2174 push @where, "cust_bill._date < $1";
2176 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2177 push @where, "cust_main.agentnum = $1";
2181 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2182 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2183 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2184 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2186 push @where, "0 != $owed"
2189 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2192 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2194 my $addl_from = 'left join cust_main using ( custnum )';
2196 if ( $param{'newest_percust'} ) {
2197 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2198 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2199 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2202 my @cust_bill = qsearch( 'cust_bill',
2204 "$distinct cust_bill.*",
2210 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2211 foreach my $cust_bill ( @cust_bill ) {
2212 $cust_bill->$method();
2214 if ( $job ) { #progressbar foo
2216 if ( time - $min_sec > $last ) {
2217 my $error = $job->update_statustext(
2218 int( 100 * $num / scalar(@cust_bill) )
2220 die $error if $error;
2235 print_text formatting (and some logic :/) is in source, but needs to be
2236 slurped in from a file. Also number of lines ($=).
2240 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2241 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base