4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
7 use Text::Template 1.20;
9 use String::ShellQuote;
12 use FS::UID qw( datasrc );
13 use FS::Record qw( qsearch qsearchs );
14 use FS::Misc qw( send_email send_fax );
16 use FS::cust_bill_pkg;
20 use FS::cust_credit_bill;
21 use FS::cust_pay_batch;
22 use FS::cust_bill_event;
24 use FS::cust_bill_pay;
25 use FS::part_bill_event;
27 @ISA = qw( FS::Record );
31 #ask FS::UID to run this stuff for us later
32 FS::UID->install_callback( sub {
34 $money_char = $conf->config('money_char') || '$';
39 FS::cust_bill - Object methods for cust_bill records
45 $record = new FS::cust_bill \%hash;
46 $record = new FS::cust_bill { 'column' => 'value' };
48 $error = $record->insert;
50 $error = $new_record->replace($old_record);
52 $error = $record->delete;
54 $error = $record->check;
56 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
58 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
60 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
62 @cust_pay_objects = $cust_bill->cust_pay;
64 $tax_amount = $record->tax;
66 @lines = $cust_bill->print_text;
67 @lines = $cust_bill->print_text $time;
71 An FS::cust_bill object represents an invoice; a declaration that a customer
72 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
73 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
74 following fields are currently supported:
78 =item invnum - primary key (assigned automatically for new invoices)
80 =item custnum - customer (see L<FS::cust_main>)
82 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
83 L<Time::Local> and L<Date::Parse> for conversion functions.
85 =item charged - amount of this invoice
87 =item printed - deprecated
89 =item closed - books closed flag, empty or `Y'
99 Creates a new invoice. To add the invoice to the database, see L<"insert">.
100 Invoices are normally created by calling the bill method of a customer object
101 (see L<FS::cust_main>).
105 sub table { 'cust_bill'; }
109 Adds this invoice to the database ("Posts" the invoice). If there is an error,
110 returns the error, otherwise returns false.
114 Currently unimplemented. I don't remove invoices because there would then be
115 no record you ever posted this invoice (which is bad, no?)
121 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
122 $self->SUPER::delete(@_);
125 =item replace OLD_RECORD
127 Replaces the OLD_RECORD with this one in the database. If there is an error,
128 returns the error, otherwise returns false.
130 Only printed may be changed. printed is normally updated by calling the
131 collect method of a customer object (see L<FS::cust_main>).
136 my( $new, $old ) = ( shift, shift );
137 return "Can't change custnum!" unless $old->custnum == $new->custnum;
138 #return "Can't change _date!" unless $old->_date eq $new->_date;
139 return "Can't change _date!" unless $old->_date == $new->_date;
140 return "Can't change charged!" unless $old->charged == $new->charged;
142 $new->SUPER::replace($old);
147 Checks all fields to make sure this is a valid invoice. If there is an error,
148 returns the error, otherwise returns false. Called by the insert and replace
157 $self->ut_numbern('invnum')
158 || $self->ut_number('custnum')
159 || $self->ut_numbern('_date')
160 || $self->ut_money('charged')
161 || $self->ut_numbern('printed')
162 || $self->ut_enum('closed', [ '', 'Y' ])
164 return $error if $error;
166 return "Unknown customer"
167 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
169 $self->_date(time) unless $self->_date;
171 $self->printed(0) if $self->printed eq '';
178 Returns a list consisting of the total previous balance for this customer,
179 followed by the previous outstanding invoices (as FS::cust_bill objects also).
186 my @cust_bill = sort { $a->_date <=> $b->_date }
187 grep { $_->owed != 0 && $_->_date < $self->_date }
188 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
190 foreach ( @cust_bill ) { $total += $_->owed; }
196 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
202 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
205 =item cust_bill_event
207 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
212 sub cust_bill_event {
214 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
220 Returns the customer (see L<FS::cust_main>) for this invoice.
226 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
231 Depreciated. See the cust_credited method.
233 #Returns a list consisting of the total previous credited (see
234 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
235 #outstanding credits (FS::cust_credit objects).
241 croak "FS::cust_bill->cust_credit depreciated; see ".
242 "FS::cust_bill->cust_credit_bill";
245 #my @cust_credit = sort { $a->_date <=> $b->_date }
246 # grep { $_->credited != 0 && $_->_date < $self->_date }
247 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
249 #foreach (@cust_credit) { $total += $_->credited; }
250 #$total, @cust_credit;
255 Depreciated. See the cust_bill_pay method.
257 #Returns all payments (see L<FS::cust_pay>) for this invoice.
263 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
265 #sort { $a->_date <=> $b->_date }
266 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
272 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
278 sort { $a->_date <=> $b->_date }
279 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
284 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
290 sort { $a->_date <=> $b->_date }
291 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
297 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
304 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
306 foreach (@taxlines) { $total += $_->setup; }
312 Returns the amount owed (still outstanding) on this invoice, which is charged
313 minus all payment applications (see L<FS::cust_bill_pay>) and credit
314 applications (see L<FS::cust_credit_bill>).
320 my $balance = $self->charged;
321 $balance -= $_->amount foreach ( $self->cust_bill_pay );
322 $balance -= $_->amount foreach ( $self->cust_credited );
323 $balance = sprintf( "%.2f", $balance);
324 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
329 =item generate_email PARAMHASH
331 PARAMHASH can contain the following:
335 =item from => sender address, required
337 =item tempate => alternate template name, optional
339 =item print_text => text attachment arrayref, optional
341 =item subject => email subject, optional
345 Returns an argument list to be passed to L<FS::Misc::send_email>.
356 my $me = '[FS::cust_bill::generate_email]';
359 'from' => $args{'from'},
360 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
363 if (ref($args{'to'} eq 'ARRAY')) {
364 $return{'to'} = $args{'to'};
366 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
367 $self->cust_main->invoicing_list
371 if ( $conf->exists('invoice_html') ) {
373 warn "$me creating HTML/text multipart message"
376 $return{'nobody'} = 1;
378 my $alternative = build MIME::Entity
379 'Type' => 'multipart/alternative',
380 'Encoding' => '7bit',
381 'Disposition' => 'inline'
385 if ( $conf->exists('invoice_email_pdf')
386 and scalar($conf->config('invoice_email_pdf_note')) ) {
388 warn "$me using 'invoice_email_pdf_note' in multipart message"
390 $data = [ map { $_ . "\n" }
391 $conf->config('invoice_email_pdf_note')
396 warn "$me not using 'invoice_email_pdf_note' in multipart message"
398 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
399 $data = $args{'print_text'};
401 $data = [ $self->print_text('', $args{'template'}) ];
406 $alternative->attach(
407 'Type' => 'text/plain',
408 #'Encoding' => 'quoted-printable',
409 'Encoding' => '7bit',
411 'Disposition' => 'inline',
414 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
415 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
417 my $image = build MIME::Entity
418 'Type' => 'image/png',
419 'Encoding' => 'base64',
420 'Path' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
421 'Filename' => 'logo.png',
422 'Content-ID' => "<$content_id>",
425 $alternative->attach(
426 'Type' => 'text/html',
427 'Encoding' => 'quoted-printable',
428 'Data' => [ '<html>',
431 ' '. encode_entities($return{'subject'}),
434 ' <body bgcolor="#e8e8e8">',
435 $self->print_html('', $args{'template'}, $content_id),
439 'Disposition' => 'inline',
440 #'Filename' => 'invoice.pdf',
443 if ( $conf->exists('invoice_email_pdf') ) {
448 # multipart/alternative
454 my $related = build MIME::Entity 'Type' => 'multipart/related',
455 'Encoding' => '7bit';
457 #false laziness w/Misc::send_email
458 $related->head->replace('Content-type',
460 '; boundary="'. $related->head->multipart_boundary. '"'.
461 '; type=multipart/alternative'
464 $related->add_part($alternative);
466 $related->add_part($image);
468 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
470 $return{'mimeparts'} = [ $related, $pdf ];
474 #no other attachment:
476 # multipart/alternative
481 $return{'content-type'} = 'multipart/related';
482 $return{'mimeparts'} = [ $alternative, $image ];
483 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
484 #$return{'disposition'} = 'inline';
490 if ( $conf->exists('invoice_email_pdf') ) {
491 warn "$me creating PDF attachment"
494 #mime parts arguments a la MIME::Entity->build().
495 $return{'mimeparts'} = [
496 { $self->mimebuild_pdf('', $args{'template'}) }
500 if ( $conf->exists('invoice_email_pdf')
501 and scalar($conf->config('invoice_email_pdf_note')) ) {
503 warn "$me using 'invoice_email_pdf_note'"
505 $return{'body'} = [ map { $_ . "\n" }
506 $conf->config('invoice_email_pdf_note')
511 warn "$me not using 'invoice_email_pdf_note'"
513 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
514 $return{'body'} = $args{'print_text'};
516 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
529 Returns a list suitable for passing to MIME::Entity->build(), representing
530 this invoice as PDF attachment.
537 'Type' => 'application/pdf',
538 'Encoding' => 'base64',
539 'Data' => [ $self->print_pdf(@_) ],
540 'Disposition' => 'attachment',
541 'Filename' => 'invoice.pdf',
545 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
547 Sends this invoice to the destinations configured for this customer: send
548 emails or print. See L<FS::cust_main_invoice>.
550 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
552 AGENTNUM, if specified, means that this invoice will only be sent for customers
553 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
554 single agent) or an arrayref of agentnums.
556 INVOICE_FROM, if specified, overrides the default email invoice From: address.
562 my $template = scalar(@_) ? shift : '';
563 if ( scalar(@_) && $_[0] ) {
564 my $agentnums = ref($_[0]) ? shift : [ shift ];
565 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
571 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
573 my @invoicing_list = $self->cust_main->invoicing_list;
575 $self->email($template, $invoice_from)
576 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
578 $self->print($template)
579 if grep { $_ eq 'POST' } @invoicing_list; #postal
581 $self->fax($template)
582 if grep { $_ eq 'FAX' } @invoicing_list; #fax
588 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
592 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
594 INVOICE_FROM, if specified, overrides the default email invoice From: address.
600 my $template = scalar(@_) ? shift : '';
604 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
606 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
607 $self->cust_main->invoicing_list;
609 #better to notify this person than silence
610 @invoicing_list = ($invoice_from) unless @invoicing_list;
612 my $error = send_email(
613 $self->generate_email(
614 'from' => $invoice_from,
615 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
616 'template' => $template,
619 die "can't email invoice: $error\n" if $error;
620 #die "$error\n" if $error;
624 =item lpr_data [ TEMPLATENAME ]
626 Returns the postscript or plaintext for this invoice.
628 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
633 my( $self, $template) = @_;
634 $conf->exists('invoice_latex')
635 ? [ $self->print_ps('', $template) ]
636 : [ $self->print_text('', $template) ];
639 =item print [ TEMPLATENAME ]
643 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
649 my $template = scalar(@_) ? shift : '';
651 my $lpr = $conf->config('lpr');
653 or die "Can't open pipe to $lpr: $!\n";
654 print LPR @{ $self->lpr_data($template) };
656 or die $! ? "Error closing $lpr: $!\n"
657 : "Exit status $? from $lpr\n";
660 =item fax [ TEMPLATENAME ]
664 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
670 my $template = scalar(@_) ? shift : '';
672 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
673 unless $conf->exists('invoice_latex');
675 my $dialstring = $self->cust_main->getfield('fax');
678 my $error = send_fax( 'docdata' => $self->lpr_data($template),
679 'dialstring' => $dialstring,
681 die $error if $error;
685 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
687 Like B<send>, but only sends the invoice if it is the newest open invoice for
697 grep { $_->owed > 0 }
698 qsearch('cust_bill', {
699 'custnum' => $self->custnum,
700 #'_date' => { op=>'>', value=>$self->_date },
701 'invnum' => { op=>'>', value=>$self->invnum },
708 =item send_csv OPTIONS
710 Sends invoice as a CSV data-file to a remote host with the specified protocol.
714 protocol - currently only "ftp"
720 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
721 and YYMMDDHHMMSS is a timestamp.
723 The fields of the CSV file is as follows:
725 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
729 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
731 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
732 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
733 fields are filled in.
735 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
736 first two fields (B<record_type> and B<invnum>) and the last five fields
737 (B<pkg> through B<edate>) are filled in.
739 =item invnum - invoice number
741 =item custnum - customer number
743 =item _date - invoice date
745 =item charged - total invoice amount
747 =item first - customer first name
749 =item last - customer first name
751 =item company - company name
753 =item address1 - address line 1
755 =item address2 - address line 1
765 =item pkg - line item description
767 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
769 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
771 =item sdate - start date for recurring fee
773 =item edate - end date for recurring fee
780 my($self, %opt) = @_;
782 #part one: create file
784 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
785 mkdir $spooldir, 0700 unless -d $spooldir;
787 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
789 open(CSV, ">$file") or die "can't open $file: $!";
791 eval "use Text::CSV_XS";
794 my $csv = Text::CSV_XS->new({'always_quote'=>1});
796 my $cust_main = $self->cust_main;
802 time2str("%x", $self->_date),
803 sprintf("%.2f", $self->charged),
804 ( map { $cust_main->getfield($_) }
805 qw( first last company address1 address2 city state zip country ) ),
807 ) or die "can't create csv";
808 print CSV $csv->string. "\n";
810 #new charges (false laziness w/print_text)
811 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
813 my($pkg, $setup, $recur, $sdate, $edate);
814 if ( $cust_bill_pkg->pkgnum ) {
816 ($pkg, $setup, $recur, $sdate, $edate) = (
817 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
818 ( $cust_bill_pkg->setup != 0
819 ? sprintf("%.2f", $cust_bill_pkg->setup )
821 ( $cust_bill_pkg->recur != 0
822 ? sprintf("%.2f", $cust_bill_pkg->recur )
824 time2str("%x", $cust_bill_pkg->sdate),
825 time2str("%x", $cust_bill_pkg->edate),
829 next unless $cust_bill_pkg->setup != 0;
830 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
831 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
833 ($pkg, $setup, $recur, $sdate, $edate) =
834 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
840 ( map { '' } (1..11) ),
841 ($pkg, $setup, $recur, $sdate, $edate)
842 ) or die "can't create csv";
843 print CSV $csv->string. "\n";
847 close CSV or die "can't close CSV: $!";
852 if ( $opt{protocol} eq 'ftp' ) {
853 eval "use Net::FTP;";
855 $net = Net::FTP->new($opt{server}) or die @$;
857 die "unknown protocol: $opt{protocol}";
860 $net->login( $opt{username}, $opt{password} )
861 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
863 $net->binary or die "can't set binary mode";
865 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
867 $net->put($file) or die "can't put $file: $!";
877 Pays this invoice with a compliemntary payment. If there is an error,
878 returns the error, otherwise returns false.
884 my $cust_pay = new FS::cust_pay ( {
885 'invnum' => $self->invnum,
886 'paid' => $self->owed,
889 'payinfo' => $self->cust_main->payinfo,
897 Attempts to pay this invoice with a credit card payment via a
898 Business::OnlinePayment realtime gateway. See
899 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
900 for supported processors.
906 $self->realtime_bop( 'CC', @_ );
911 Attempts to pay this invoice with an electronic check (ACH) payment via a
912 Business::OnlinePayment realtime gateway. See
913 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
914 for supported processors.
920 $self->realtime_bop( 'ECHECK', @_ );
925 Attempts to pay this invoice with phone bill (LEC) payment via a
926 Business::OnlinePayment realtime gateway. See
927 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
928 for supported processors.
934 $self->realtime_bop( 'LEC', @_ );
938 my( $self, $method ) = @_;
940 my $cust_main = $self->cust_main;
941 my $balance = $cust_main->balance;
942 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
943 $amount = sprintf("%.2f", $amount);
944 return "not run (balance $balance)" unless $amount > 0;
946 my $description = 'Internet Services';
947 if ( $conf->exists('business-onlinepayment-description') ) {
948 my $dtempl = $conf->config('business-onlinepayment-description');
950 my $agent_obj = $cust_main->agent
951 or die "can't retreive agent for $cust_main (agentnum ".
952 $cust_main->agentnum. ")";
953 my $agent = $agent_obj->agent;
954 my $pkgs = join(', ',
955 map { $_->cust_pkg->part_pkg->pkg }
956 grep { $_->pkgnum } $self->cust_bill_pkg
958 $description = eval qq("$dtempl");
961 $cust_main->realtime_bop($method, $amount,
962 'description' => $description,
963 'invnum' => $self->invnum,
970 Adds a payment for this invoice to the pending credit card batch (see
971 L<FS::cust_pay_batch>).
977 my $cust_main = $self->cust_main;
979 my $cust_pay_batch = new FS::cust_pay_batch ( {
980 'invnum' => $self->getfield('invnum'),
981 'custnum' => $cust_main->getfield('custnum'),
982 'last' => $cust_main->getfield('last'),
983 'first' => $cust_main->getfield('first'),
984 'address1' => $cust_main->getfield('address1'),
985 'address2' => $cust_main->getfield('address2'),
986 'city' => $cust_main->getfield('city'),
987 'state' => $cust_main->getfield('state'),
988 'zip' => $cust_main->getfield('zip'),
989 'country' => $cust_main->getfield('country'),
990 'cardnum' => $cust_main->payinfo,
991 'exp' => $cust_main->getfield('paydate'),
992 'payname' => $cust_main->getfield('payname'),
993 'amount' => $self->owed,
995 my $error = $cust_pay_batch->insert;
996 die $error if $error;
1001 sub _agent_template {
1003 $self->_agent_plandata('agent_templatename');
1006 sub _agent_invoice_from {
1008 $self->_agent_plandata('agent_invoice_from');
1011 sub _agent_plandata {
1012 my( $self, $option ) = @_;
1014 my $part_bill_event = qsearchs( 'part_bill_event',
1016 'payby' => $self->cust_main->payby,
1017 'plan' => 'send_agent',
1018 'plandata' => { 'op' => '~',
1019 'value' => "(^|\n)agentnum ".
1020 $self->cust_main->agentnum.
1025 'ORDER BY seconds LIMIT 1'
1028 return '' unless $part_bill_event;
1030 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1033 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1034 " plandata for $option";
1040 =item print_text [ TIME [ , TEMPLATE ] ]
1042 Returns an text invoice, as a list of lines.
1044 TIME an optional value used to control the printing of overdue messages. The
1045 default is now. It isn't the date of the invoice; that's the `_date' field.
1046 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1047 L<Time::Local> and L<Date::Parse> for conversion functions.
1051 #still some false laziness w/print_text
1054 my( $self, $today, $template ) = @_;
1057 # my $invnum = $self->invnum;
1058 my $cust_main = $self->cust_main;
1059 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1060 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1062 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1063 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1064 #my $balance_due = $self->owed + $pr_total - $cr_total;
1065 my $balance_due = $self->owed + $pr_total;
1068 #my($description,$amount);
1072 foreach ( @pr_cust_bill ) {
1074 "Previous Balance, Invoice #". $_->invnum.
1075 " (". time2str("%x",$_->_date). ")",
1076 $money_char. sprintf("%10.2f",$_->owed)
1079 if (@pr_cust_bill) {
1080 push @buf,['','-----------'];
1081 push @buf,[ 'Total Previous Balance',
1082 $money_char. sprintf("%10.2f",$pr_total ) ];
1087 foreach my $cust_bill_pkg (
1088 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1089 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1092 if ( $cust_bill_pkg->pkgnum > 0 ) {
1094 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1095 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1096 my $pkg = $part_pkg->pkg;
1098 if ( $cust_bill_pkg->setup != 0 ) {
1099 my $description = $pkg;
1100 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1101 push @buf, [ $description,
1102 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1104 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1105 $cust_pkg->h_labels($self->_date);
1108 if ( $cust_bill_pkg->recur != 0 ) {
1110 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1111 time2str("%x", $cust_bill_pkg->edate) . ")",
1112 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1115 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1116 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1119 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1121 } else { #pkgnum tax or one-shot line item
1122 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1123 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1125 if ( $cust_bill_pkg->setup != 0 ) {
1126 push @buf, [ $itemdesc,
1127 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1129 if ( $cust_bill_pkg->recur != 0 ) {
1130 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1131 . time2str("%x", $cust_bill_pkg->edate). ")",
1132 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1138 push @buf,['','-----------'];
1139 push @buf,['Total New Charges',
1140 $money_char. sprintf("%10.2f",$self->charged) ];
1143 push @buf,['','-----------'];
1144 push @buf,['Total Charges',
1145 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1149 foreach ( $self->cust_credited ) {
1151 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1153 my $reason = substr($_->cust_credit->reason,0,32);
1154 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1155 $reason = " ($reason) " if $reason;
1157 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1159 $money_char. sprintf("%10.2f",$_->amount)
1162 #foreach ( @cr_cust_credit ) {
1164 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1165 # $money_char. sprintf("%10.2f",$_->credited)
1169 #get & print payments
1170 foreach ( $self->cust_bill_pay ) {
1172 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1175 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1176 $money_char. sprintf("%10.2f",$_->amount )
1181 my $balance_due_msg = $self->balance_due_msg;
1183 push @buf,['','-----------'];
1184 push @buf,[$balance_due_msg, $money_char.
1185 sprintf("%10.2f", $balance_due ) ];
1187 #create the template
1188 $template ||= $self->_agent_template;
1189 my $templatefile = 'invoice_template';
1190 $templatefile .= "_$template" if length($template);
1191 my @invoice_template = $conf->config($templatefile)
1192 or die "cannot load config file $templatefile";
1195 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1196 /invoice_lines\((\d*)\)/;
1197 $invoice_lines += $1 || scalar(@buf);
1200 die "no invoice_lines() functions in template?" unless $wasfunc;
1201 my $invoice_template = new Text::Template (
1203 SOURCE => [ map "$_\n", @invoice_template ],
1204 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1205 $invoice_template->compile()
1206 or die "can't compile template: $Text::Template::ERROR";
1208 #setup template variables
1209 package FS::cust_bill::_template; #!
1210 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1212 $invnum = $self->invnum;
1213 $date = $self->_date;
1215 $agent = $self->cust_main->agent->agent;
1217 if ( $FS::cust_bill::invoice_lines ) {
1219 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1221 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1226 #format address (variable for the template)
1228 @address = ( '', '', '', '', '', '' );
1229 package FS::cust_bill; #!
1230 $FS::cust_bill::_template::address[$l++] =
1231 $cust_main->payname.
1232 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1233 ? " (P.O. #". $cust_main->payinfo. ")"
1237 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1238 if $cust_main->company;
1239 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1240 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1241 if $cust_main->address2;
1242 $FS::cust_bill::_template::address[$l++] =
1243 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1245 my $countrydefault = $conf->config('countrydefault') || 'US';
1246 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1247 unless $cust_main->country eq $countrydefault;
1249 # #overdue? (variable for the template)
1250 # $FS::cust_bill::_template::overdue = (
1252 # && $today > $self->_date
1253 ## && $self->printed > 1
1254 # && $self->printed > 0
1257 #and subroutine for the template
1258 sub FS::cust_bill::_template::invoice_lines {
1259 my $lines = shift || scalar(@buf);
1261 scalar(@buf) ? shift @buf : [ '', '' ];
1267 $FS::cust_bill::_template::page = 1;
1271 push @collect, split("\n",
1272 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1274 $FS::cust_bill::_template::page++;
1277 map "$_\n", @collect;
1281 =item print_latex [ TIME [ , TEMPLATE ] ]
1283 Internal method - returns a filename of a filled-in LaTeX template for this
1284 invoice (Note: add ".tex" to get the actual filename).
1286 See print_ps and print_pdf for methods that return PostScript and PDF output.
1288 TIME an optional value used to control the printing of overdue messages. The
1289 default is now. It isn't the date of the invoice; that's the `_date' field.
1290 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1291 L<Time::Local> and L<Date::Parse> for conversion functions.
1295 #still some false laziness w/print_text
1298 my( $self, $today, $template ) = @_;
1300 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1303 my $cust_main = $self->cust_main;
1304 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1305 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1307 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1308 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1309 #my $balance_due = $self->owed + $pr_total - $cr_total;
1310 my $balance_due = $self->owed + $pr_total;
1312 #create the template
1313 $template ||= $self->_agent_template;
1314 my $templatefile = 'invoice_latex';
1315 my $suffix = length($template) ? "_$template" : '';
1316 $templatefile .= $suffix;
1317 my @invoice_template = map "$_\n", $conf->config($templatefile)
1318 or die "cannot load config file $templatefile";
1320 my($format, $text_template);
1321 if ( grep { /^%%Detail/ } @invoice_template ) {
1322 #change this to a die when the old code is removed
1323 warn "old-style invoice template $templatefile; ".
1324 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1327 $format = 'Text::Template';
1328 $text_template = new Text::Template(
1330 SOURCE => \@invoice_template,
1331 DELIMITERS => [ '[@--', '--@]' ],
1334 $text_template->compile()
1335 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1339 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1340 $returnaddress = join("\n",
1341 $conf->config_orbase('invoice_latexreturnaddress', $template)
1344 $returnaddress = '~';
1347 my %invoice_data = (
1348 'invnum' => $self->invnum,
1349 'date' => time2str('%b %o, %Y', $self->_date),
1350 'today' => time2str('%b %o, %Y', $today),
1351 'agent' => _latex_escape($cust_main->agent->agent),
1352 'payname' => _latex_escape($cust_main->payname),
1353 'company' => _latex_escape($cust_main->company),
1354 'address1' => _latex_escape($cust_main->address1),
1355 'address2' => _latex_escape($cust_main->address2),
1356 'city' => _latex_escape($cust_main->city),
1357 'state' => _latex_escape($cust_main->state),
1358 'zip' => _latex_escape($cust_main->zip),
1359 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1360 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1361 'returnaddress' => $returnaddress,
1363 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1364 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1365 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1368 my $countrydefault = $conf->config('countrydefault') || 'US';
1369 if ( $cust_main->country eq $countrydefault ) {
1370 $invoice_data{'country'} = '';
1372 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1375 $invoice_data{'notes'} =
1377 # #do variable substitutions in notes
1378 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1379 $conf->config_orbase('invoice_latexnotes', $template)
1381 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1384 $invoice_data{'footer'} =~ s/\n+$//;
1385 $invoice_data{'smallfooter'} =~ s/\n+$//;
1386 $invoice_data{'notes'} =~ s/\n+$//;
1388 $invoice_data{'po_line'} =
1389 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1390 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1394 if ( $format eq 'old' ) {
1397 my @total_item = ();
1398 while ( @invoice_template ) {
1399 my $line = shift @invoice_template;
1401 if ( $line =~ /^%%Detail\s*$/ ) {
1403 while ( ( my $line_item_line = shift @invoice_template )
1404 !~ /^%%EndDetail\s*$/ ) {
1405 push @line_item, $line_item_line;
1407 foreach my $line_item ( $self->_items ) {
1408 #foreach my $line_item ( $self->_items_pkg ) {
1409 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1410 $invoice_data{'description'} =
1411 _latex_escape($line_item->{'description'});
1412 if ( exists $line_item->{'ext_description'} ) {
1413 $invoice_data{'description'} .=
1414 "\\tabularnewline\n~~".
1415 join( "\\tabularnewline\n~~",
1416 map _latex_escape($_), @{$line_item->{'ext_description'}}
1419 $invoice_data{'amount'} = $line_item->{'amount'};
1420 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1422 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1425 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1427 while ( ( my $total_item_line = shift @invoice_template )
1428 !~ /^%%EndTotalDetails\s*$/ ) {
1429 push @total_item, $total_item_line;
1432 my @total_fill = ();
1435 foreach my $tax ( $self->_items_tax ) {
1436 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1437 $taxtotal += $tax->{'amount'};
1438 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1440 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1445 $invoice_data{'total_item'} = 'Sub-total';
1446 $invoice_data{'total_amount'} =
1447 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1448 unshift @total_fill,
1449 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1453 $invoice_data{'total_item'} = '\textbf{Total}';
1454 $invoice_data{'total_amount'} =
1455 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1457 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1460 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1463 foreach my $credit ( $self->_items_credits ) {
1464 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1466 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1468 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1473 foreach my $payment ( $self->_items_payments ) {
1474 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1476 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1478 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1482 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1483 $invoice_data{'total_amount'} =
1484 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1486 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1489 push @filled_in, @total_fill;
1492 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1493 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1494 push @filled_in, $line;
1505 } elsif ( $format eq 'Text::Template' ) {
1507 my @detail_items = ();
1508 my @total_items = ();
1510 $invoice_data{'detail_items'} = \@detail_items;
1511 $invoice_data{'total_items'} = \@total_items;
1513 foreach my $line_item ( $self->_items ) {
1515 ext_description => [],
1517 $detail->{'ref'} = $line_item->{'pkgnum'};
1518 $detail->{'quantity'} = 1;
1519 $detail->{'description'} = _latex_escape($line_item->{'description'});
1520 if ( exists $line_item->{'ext_description'} ) {
1521 @{$detail->{'ext_description'}} = map {
1523 } @{$line_item->{'ext_description'}};
1525 $detail->{'amount'} = $line_item->{'amount'};
1526 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1528 push @detail_items, $detail;
1533 foreach my $tax ( $self->_items_tax ) {
1535 $total->{'total_item'} = _latex_escape($tax->{'description'});
1536 $taxtotal += $tax->{'amount'};
1537 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1538 push @total_items, $total;
1543 $total->{'total_item'} = 'Sub-total';
1544 $total->{'total_amount'} =
1545 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1546 unshift @total_items, $total;
1551 $total->{'total_item'} = '\textbf{Total}';
1552 $total->{'total_amount'} =
1553 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1554 push @total_items, $total;
1557 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1560 foreach my $credit ( $self->_items_credits ) {
1562 $total->{'total_item'} = _latex_escape($credit->{'description'});
1564 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1565 push @total_items, $total;
1569 foreach my $payment ( $self->_items_payments ) {
1571 $total->{'total_item'} = _latex_escape($payment->{'description'});
1573 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1574 push @total_items, $total;
1579 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1580 $total->{'total_amount'} =
1581 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1582 push @total_items, $total;
1586 die "guru meditation #54";
1589 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1590 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1594 ) or die "can't open temp file: $!\n";
1595 if ( $format eq 'old' ) {
1596 print $fh join('', @filled_in );
1597 } elsif ( $format eq 'Text::Template' ) {
1598 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1600 die "guru meditation #32";
1604 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1609 =item print_ps [ TIME [ , TEMPLATE ] ]
1611 Returns an postscript invoice, as a scalar.
1613 TIME an optional value used to control the printing of overdue messages. The
1614 default is now. It isn't the date of the invoice; that's the `_date' field.
1615 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1616 L<Time::Local> and L<Date::Parse> for conversion functions.
1623 my $file = $self->print_latex(@_);
1625 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1628 my $sfile = shell_quote $file;
1630 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1631 or die "pslatex $file.tex failed; see $file.log for details?\n";
1632 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1633 or die "pslatex $file.tex failed; see $file.log for details?\n";
1635 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1636 or die "dvips failed";
1638 open(POSTSCRIPT, "<$file.ps")
1639 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1641 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1644 while (<POSTSCRIPT>) {
1654 =item print_pdf [ TIME [ , TEMPLATE ] ]
1656 Returns an PDF invoice, as a scalar.
1658 TIME an optional value used to control the printing of overdue messages. The
1659 default is now. It isn't the date of the invoice; that's the `_date' field.
1660 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1661 L<Time::Local> and L<Date::Parse> for conversion functions.
1668 my $file = $self->print_latex(@_);
1670 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1673 #system('pdflatex', "$file.tex");
1674 #system('pdflatex', "$file.tex");
1675 #! LaTeX Error: Unknown graphics extension: .eps.
1677 my $sfile = shell_quote $file;
1679 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1680 or die "pslatex $file.tex failed; see $file.log for details?\n";
1681 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1682 or die "pslatex $file.tex failed; see $file.log for details?\n";
1684 #system('dvipdf', "$file.dvi", "$file.pdf" );
1686 "dvips -q -t letter -f $sfile.dvi ".
1687 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1690 or die "dvips | gs failed: $!";
1692 open(PDF, "<$file.pdf")
1693 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1695 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1708 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1710 Returns an HTML invoice, as a scalar.
1712 TIME an optional value used to control the printing of overdue messages. The
1713 default is now. It isn't the date of the invoice; that's the `_date' field.
1714 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1715 L<Time::Local> and L<Date::Parse> for conversion functions.
1717 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1718 when emailing the invoice as part of a multipart/related MIME email.
1723 my( $self, $today, $template, $cid ) = @_;
1726 my $cust_main = $self->cust_main;
1727 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1728 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1730 $template ||= $self->_agent_template;
1731 my $templatefile = 'invoice_html';
1732 my $suffix = length($template) ? "_$template" : '';
1733 $templatefile .= $suffix;
1734 my @html_template = map "$_\n", $conf->config($templatefile)
1735 or die "cannot load config file $templatefile";
1737 my $html_template = new Text::Template(
1739 SOURCE => \@html_template,
1740 DELIMITERS => [ '<%=', '%>' ],
1743 $html_template->compile()
1744 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1746 my %invoice_data = (
1747 'invnum' => $self->invnum,
1748 'date' => time2str('%b %o, %Y', $self->_date),
1749 'today' => time2str('%b %o, %Y', $today),
1750 'agent' => encode_entities($cust_main->agent->agent),
1751 'payname' => encode_entities($cust_main->payname),
1752 'company' => encode_entities($cust_main->company),
1753 'address1' => encode_entities($cust_main->address1),
1754 'address2' => encode_entities($cust_main->address2),
1755 'city' => encode_entities($cust_main->city),
1756 'state' => encode_entities($cust_main->state),
1757 'zip' => encode_entities($cust_main->zip),
1758 'terms' => $conf->config('invoice_default_terms')
1759 || 'Payable upon receipt',
1761 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1764 $invoice_data{'returnaddress'} =
1765 length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1766 ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1769 s/\\\\\*?\s*$/<BR>/;
1770 s/\\hyphenation\{[\w\s\-]+\}//;
1773 $conf->config_orbase('invoice_latexreturnaddress', $template)
1776 my $countrydefault = $conf->config('countrydefault') || 'US';
1777 if ( $cust_main->country eq $countrydefault ) {
1778 $invoice_data{'country'} = '';
1780 $invoice_data{'country'} =
1781 encode_entities(code2country($cust_main->country));
1784 $invoice_data{'notes'} =
1785 length($conf->config_orbase('invoice_htmlnotes', $template))
1786 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1788 s/%%(.*)$/<!-- $1 -->/;
1789 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1790 s/\\begin\{enumerate\}/<ol>/;
1792 s/\\end\{enumerate\}/<\/ol>/;
1793 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1796 $conf->config_orbase('invoice_latexnotes', $template)
1799 # #do variable substitutions in notes
1800 # $invoice_data{'notes'} =
1802 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1803 # $conf->config_orbase('invoice_latexnotes', $suffix)
1806 $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1807 ? join("\n", $conf->config('invoice_htmlfooter') )
1808 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1809 $conf->config('invoice_latexfooter')
1812 $invoice_data{'po_line'} =
1813 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1814 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1817 my $money_char = $conf->config('money_char') || '$';
1819 foreach my $line_item ( $self->_items ) {
1821 ext_description => [],
1823 $detail->{'ref'} = $line_item->{'pkgnum'};
1824 $detail->{'description'} = encode_entities($line_item->{'description'});
1825 if ( exists $line_item->{'ext_description'} ) {
1826 @{$detail->{'ext_description'}} = map {
1827 encode_entities($_);
1828 } @{$line_item->{'ext_description'}};
1830 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1831 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1833 push @{$invoice_data{'detail_items'}}, $detail;
1838 foreach my $tax ( $self->_items_tax ) {
1840 $total->{'total_item'} = encode_entities($tax->{'description'});
1841 $taxtotal += $tax->{'amount'};
1842 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1843 push @{$invoice_data{'total_items'}}, $total;
1848 $total->{'total_item'} = 'Sub-total';
1849 $total->{'total_amount'} =
1850 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1851 unshift @{$invoice_data{'total_items'}}, $total;
1854 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1857 $total->{'total_item'} = '<b>Total</b>';
1858 $total->{'total_amount'} =
1859 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1860 push @{$invoice_data{'total_items'}}, $total;
1863 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1866 foreach my $credit ( $self->_items_credits ) {
1868 $total->{'total_item'} = encode_entities($credit->{'description'});
1870 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1871 push @{$invoice_data{'total_items'}}, $total;
1875 foreach my $payment ( $self->_items_payments ) {
1877 $total->{'total_item'} = encode_entities($payment->{'description'});
1879 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1880 push @{$invoice_data{'total_items'}}, $total;
1885 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1886 $total->{'total_amount'} =
1887 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1888 push @{$invoice_data{'total_items'}}, $total;
1891 $html_template->fill_in( HASH => \%invoice_data);
1894 # quick subroutine for print_latex
1896 # There are ten characters that LaTeX treats as special characters, which
1897 # means that they do not simply typeset themselves:
1898 # # $ % & ~ _ ^ \ { }
1900 # TeX ignores blanks following an escaped character; if you want a blank (as
1901 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1905 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1906 $value =~ s/([<>])/\$$1\$/g;
1910 #utility methods for print_*
1912 sub balance_due_msg {
1914 my $msg = 'Balance Due';
1915 return $msg unless $conf->exists('invoice_default_terms');
1916 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1917 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1918 } elsif ( $conf->config('invoice_default_terms') ) {
1919 $msg .= ' - '. $conf->config('invoice_default_terms');
1926 my @display = scalar(@_)
1928 : qw( _items_previous _items_pkg );
1929 #: qw( _items_pkg );
1930 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1932 foreach my $display ( @display ) {
1933 push @b, $self->$display(@_);
1938 sub _items_previous {
1940 my $cust_main = $self->cust_main;
1941 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1943 foreach ( @pr_cust_bill ) {
1945 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1946 ' ('. time2str('%x',$_->_date). ')',
1947 #'pkgpart' => 'N/A',
1949 'amount' => sprintf("%.2f", $_->owed),
1955 # 'description' => 'Previous Balance',
1956 # #'pkgpart' => 'N/A',
1957 # 'pkgnum' => 'N/A',
1958 # 'amount' => sprintf("%10.2f", $pr_total ),
1959 # 'ext_description' => [ map {
1960 # "Invoice ". $_->invnum.
1961 # " (". time2str("%x",$_->_date). ") ".
1962 # sprintf("%10.2f", $_->owed)
1963 # } @pr_cust_bill ],
1970 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1971 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1976 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1977 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1980 sub _items_cust_bill_pkg {
1982 my $cust_bill_pkg = shift;
1985 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1987 if ( $cust_bill_pkg->pkgnum > 0 ) {
1989 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1990 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1991 my $pkg = $part_pkg->pkg;
1993 if ( $cust_bill_pkg->setup != 0 ) {
1994 my $description = $pkg;
1995 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1996 my @d = $cust_pkg->h_labels_short($self->_date);
1997 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1999 description => $description,
2000 #pkgpart => $part_pkg->pkgpart,
2001 pkgnum => $cust_pkg->pkgnum,
2002 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2003 ext_description => \@d,
2007 if ( $cust_bill_pkg->recur != 0 ) {
2009 description => "$pkg (" .
2010 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2011 time2str('%x', $cust_bill_pkg->edate). ')',
2012 #pkgpart => $part_pkg->pkgpart,
2013 pkgnum => $cust_pkg->pkgnum,
2014 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2015 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2016 $cust_bill_pkg->sdate),
2017 $cust_bill_pkg->details,
2022 } else { #pkgnum tax or one-shot line item (??)
2024 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2025 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2027 if ( $cust_bill_pkg->setup != 0 ) {
2029 'description' => $itemdesc,
2030 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2033 if ( $cust_bill_pkg->recur != 0 ) {
2035 'description' => "$itemdesc (".
2036 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2037 time2str("%x", $cust_bill_pkg->edate). ')',
2038 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2050 sub _items_credits {
2055 foreach ( $self->cust_credited ) {
2057 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2059 my $reason = $_->cust_credit->reason;
2060 #my $reason = substr($_->cust_credit->reason,0,32);
2061 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2062 $reason = " ($reason) " if $reason;
2064 #'description' => 'Credit ref\#'. $_->crednum.
2065 # " (". time2str("%x",$_->cust_credit->_date) .")".
2067 'description' => 'Credit applied '.
2068 time2str("%x",$_->cust_credit->_date). $reason,
2069 'amount' => sprintf("%.2f",$_->amount),
2072 #foreach ( @cr_cust_credit ) {
2074 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2075 # $money_char. sprintf("%10.2f",$_->credited)
2083 sub _items_payments {
2087 #get & print payments
2088 foreach ( $self->cust_bill_pay ) {
2090 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2093 'description' => "Payment received ".
2094 time2str("%x",$_->cust_pay->_date ),
2095 'amount' => sprintf("%.2f", $_->amount )
2109 print_text formatting (and some logic :/) is in source, but needs to be
2110 slurped in from a file. Also number of lines ($=).
2114 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2115 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base