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 ".
1021 '[ ,]'. $self->cust_main->agentnum. '[ ,]'
1027 'ORDER BY seconds LIMIT 1'
1030 return '' unless $part_bill_event;
1032 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1035 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1036 " plandata for $option";
1042 =item print_text [ TIME [ , TEMPLATE ] ]
1044 Returns an text invoice, as a list of lines.
1046 TIME an optional value used to control the printing of overdue messages. The
1047 default is now. It isn't the date of the invoice; that's the `_date' field.
1048 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1049 L<Time::Local> and L<Date::Parse> for conversion functions.
1053 #still some false laziness w/print_text
1056 my( $self, $today, $template ) = @_;
1059 # my $invnum = $self->invnum;
1060 my $cust_main = $self->cust_main;
1061 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1062 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1064 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1065 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1066 #my $balance_due = $self->owed + $pr_total - $cr_total;
1067 my $balance_due = $self->owed + $pr_total;
1070 #my($description,$amount);
1074 foreach ( @pr_cust_bill ) {
1076 "Previous Balance, Invoice #". $_->invnum.
1077 " (". time2str("%x",$_->_date). ")",
1078 $money_char. sprintf("%10.2f",$_->owed)
1081 if (@pr_cust_bill) {
1082 push @buf,['','-----------'];
1083 push @buf,[ 'Total Previous Balance',
1084 $money_char. sprintf("%10.2f",$pr_total ) ];
1089 foreach my $cust_bill_pkg (
1090 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1091 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1094 if ( $cust_bill_pkg->pkgnum > 0 ) {
1096 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1097 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1098 my $pkg = $part_pkg->pkg;
1100 if ( $cust_bill_pkg->setup != 0 ) {
1101 my $description = $pkg;
1102 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1103 push @buf, [ $description,
1104 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1106 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1107 $cust_pkg->h_labels($self->_date);
1110 if ( $cust_bill_pkg->recur != 0 ) {
1112 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1113 time2str("%x", $cust_bill_pkg->edate) . ")",
1114 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1117 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1118 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1121 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1123 } else { #pkgnum tax or one-shot line item
1124 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1125 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1127 if ( $cust_bill_pkg->setup != 0 ) {
1128 push @buf, [ $itemdesc,
1129 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1131 if ( $cust_bill_pkg->recur != 0 ) {
1132 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1133 . time2str("%x", $cust_bill_pkg->edate). ")",
1134 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1140 push @buf,['','-----------'];
1141 push @buf,['Total New Charges',
1142 $money_char. sprintf("%10.2f",$self->charged) ];
1145 push @buf,['','-----------'];
1146 push @buf,['Total Charges',
1147 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1151 foreach ( $self->cust_credited ) {
1153 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1155 my $reason = substr($_->cust_credit->reason,0,32);
1156 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1157 $reason = " ($reason) " if $reason;
1159 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1161 $money_char. sprintf("%10.2f",$_->amount)
1164 #foreach ( @cr_cust_credit ) {
1166 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1167 # $money_char. sprintf("%10.2f",$_->credited)
1171 #get & print payments
1172 foreach ( $self->cust_bill_pay ) {
1174 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1177 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1178 $money_char. sprintf("%10.2f",$_->amount )
1183 my $balance_due_msg = $self->balance_due_msg;
1185 push @buf,['','-----------'];
1186 push @buf,[$balance_due_msg, $money_char.
1187 sprintf("%10.2f", $balance_due ) ];
1189 #create the template
1190 $template ||= $self->_agent_template;
1191 my $templatefile = 'invoice_template';
1192 $templatefile .= "_$template" if length($template);
1193 my @invoice_template = $conf->config($templatefile)
1194 or die "cannot load config file $templatefile";
1197 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1198 /invoice_lines\((\d*)\)/;
1199 $invoice_lines += $1 || scalar(@buf);
1202 die "no invoice_lines() functions in template?" unless $wasfunc;
1203 my $invoice_template = new Text::Template (
1205 SOURCE => [ map "$_\n", @invoice_template ],
1206 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1207 $invoice_template->compile()
1208 or die "can't compile template: $Text::Template::ERROR";
1210 #setup template variables
1211 package FS::cust_bill::_template; #!
1212 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1214 $invnum = $self->invnum;
1215 $date = $self->_date;
1217 $agent = $self->cust_main->agent->agent;
1219 if ( $FS::cust_bill::invoice_lines ) {
1221 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1223 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1228 #format address (variable for the template)
1230 @address = ( '', '', '', '', '', '' );
1231 package FS::cust_bill; #!
1232 $FS::cust_bill::_template::address[$l++] =
1233 $cust_main->payname.
1234 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1235 ? " (P.O. #". $cust_main->payinfo. ")"
1239 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1240 if $cust_main->company;
1241 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1242 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1243 if $cust_main->address2;
1244 $FS::cust_bill::_template::address[$l++] =
1245 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1247 my $countrydefault = $conf->config('countrydefault') || 'US';
1248 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1249 unless $cust_main->country eq $countrydefault;
1251 # #overdue? (variable for the template)
1252 # $FS::cust_bill::_template::overdue = (
1254 # && $today > $self->_date
1255 ## && $self->printed > 1
1256 # && $self->printed > 0
1259 #and subroutine for the template
1260 sub FS::cust_bill::_template::invoice_lines {
1261 my $lines = shift || scalar(@buf);
1263 scalar(@buf) ? shift @buf : [ '', '' ];
1269 $FS::cust_bill::_template::page = 1;
1273 push @collect, split("\n",
1274 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1276 $FS::cust_bill::_template::page++;
1279 map "$_\n", @collect;
1283 =item print_latex [ TIME [ , TEMPLATE ] ]
1285 Internal method - returns a filename of a filled-in LaTeX template for this
1286 invoice (Note: add ".tex" to get the actual filename).
1288 See print_ps and print_pdf for methods that return PostScript and PDF output.
1290 TIME an optional value used to control the printing of overdue messages. The
1291 default is now. It isn't the date of the invoice; that's the `_date' field.
1292 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1293 L<Time::Local> and L<Date::Parse> for conversion functions.
1297 #still some false laziness w/print_text
1300 my( $self, $today, $template ) = @_;
1302 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1305 my $cust_main = $self->cust_main;
1306 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1307 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1309 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1310 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1311 #my $balance_due = $self->owed + $pr_total - $cr_total;
1312 my $balance_due = $self->owed + $pr_total;
1314 #create the template
1315 $template ||= $self->_agent_template;
1316 my $templatefile = 'invoice_latex';
1317 my $suffix = length($template) ? "_$template" : '';
1318 $templatefile .= $suffix;
1319 my @invoice_template = map "$_\n", $conf->config($templatefile)
1320 or die "cannot load config file $templatefile";
1322 my($format, $text_template);
1323 if ( grep { /^%%Detail/ } @invoice_template ) {
1324 #change this to a die when the old code is removed
1325 warn "old-style invoice template $templatefile; ".
1326 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1329 $format = 'Text::Template';
1330 $text_template = new Text::Template(
1332 SOURCE => \@invoice_template,
1333 DELIMITERS => [ '[@--', '--@]' ],
1336 $text_template->compile()
1337 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1341 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1342 $returnaddress = join("\n",
1343 $conf->config_orbase('invoice_latexreturnaddress', $template)
1346 $returnaddress = '~';
1349 my %invoice_data = (
1350 'invnum' => $self->invnum,
1351 'date' => time2str('%b %o, %Y', $self->_date),
1352 'today' => time2str('%b %o, %Y', $today),
1353 'agent' => _latex_escape($cust_main->agent->agent),
1354 'payname' => _latex_escape($cust_main->payname),
1355 'company' => _latex_escape($cust_main->company),
1356 'address1' => _latex_escape($cust_main->address1),
1357 'address2' => _latex_escape($cust_main->address2),
1358 'city' => _latex_escape($cust_main->city),
1359 'state' => _latex_escape($cust_main->state),
1360 'zip' => _latex_escape($cust_main->zip),
1361 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1362 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1363 'returnaddress' => $returnaddress,
1365 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1366 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1367 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1370 my $countrydefault = $conf->config('countrydefault') || 'US';
1371 if ( $cust_main->country eq $countrydefault ) {
1372 $invoice_data{'country'} = '';
1374 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1377 $invoice_data{'notes'} =
1379 # #do variable substitutions in notes
1380 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1381 $conf->config_orbase('invoice_latexnotes', $template)
1383 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1386 $invoice_data{'footer'} =~ s/\n+$//;
1387 $invoice_data{'smallfooter'} =~ s/\n+$//;
1388 $invoice_data{'notes'} =~ s/\n+$//;
1390 $invoice_data{'po_line'} =
1391 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1392 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1396 if ( $format eq 'old' ) {
1399 my @total_item = ();
1400 while ( @invoice_template ) {
1401 my $line = shift @invoice_template;
1403 if ( $line =~ /^%%Detail\s*$/ ) {
1405 while ( ( my $line_item_line = shift @invoice_template )
1406 !~ /^%%EndDetail\s*$/ ) {
1407 push @line_item, $line_item_line;
1409 foreach my $line_item ( $self->_items ) {
1410 #foreach my $line_item ( $self->_items_pkg ) {
1411 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1412 $invoice_data{'description'} =
1413 _latex_escape($line_item->{'description'});
1414 if ( exists $line_item->{'ext_description'} ) {
1415 $invoice_data{'description'} .=
1416 "\\tabularnewline\n~~".
1417 join( "\\tabularnewline\n~~",
1418 map _latex_escape($_), @{$line_item->{'ext_description'}}
1421 $invoice_data{'amount'} = $line_item->{'amount'};
1422 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1424 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1427 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1429 while ( ( my $total_item_line = shift @invoice_template )
1430 !~ /^%%EndTotalDetails\s*$/ ) {
1431 push @total_item, $total_item_line;
1434 my @total_fill = ();
1437 foreach my $tax ( $self->_items_tax ) {
1438 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1439 $taxtotal += $tax->{'amount'};
1440 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1442 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1447 $invoice_data{'total_item'} = 'Sub-total';
1448 $invoice_data{'total_amount'} =
1449 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1450 unshift @total_fill,
1451 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1455 $invoice_data{'total_item'} = '\textbf{Total}';
1456 $invoice_data{'total_amount'} =
1457 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1459 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1462 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1465 foreach my $credit ( $self->_items_credits ) {
1466 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1468 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1470 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1475 foreach my $payment ( $self->_items_payments ) {
1476 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1478 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1480 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1484 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1485 $invoice_data{'total_amount'} =
1486 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1488 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1491 push @filled_in, @total_fill;
1494 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1495 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1496 push @filled_in, $line;
1507 } elsif ( $format eq 'Text::Template' ) {
1509 my @detail_items = ();
1510 my @total_items = ();
1512 $invoice_data{'detail_items'} = \@detail_items;
1513 $invoice_data{'total_items'} = \@total_items;
1515 foreach my $line_item ( $self->_items ) {
1517 ext_description => [],
1519 $detail->{'ref'} = $line_item->{'pkgnum'};
1520 $detail->{'quantity'} = 1;
1521 $detail->{'description'} = _latex_escape($line_item->{'description'});
1522 if ( exists $line_item->{'ext_description'} ) {
1523 @{$detail->{'ext_description'}} = map {
1525 } @{$line_item->{'ext_description'}};
1527 $detail->{'amount'} = $line_item->{'amount'};
1528 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1530 push @detail_items, $detail;
1535 foreach my $tax ( $self->_items_tax ) {
1537 $total->{'total_item'} = _latex_escape($tax->{'description'});
1538 $taxtotal += $tax->{'amount'};
1539 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1540 push @total_items, $total;
1545 $total->{'total_item'} = 'Sub-total';
1546 $total->{'total_amount'} =
1547 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1548 unshift @total_items, $total;
1553 $total->{'total_item'} = '\textbf{Total}';
1554 $total->{'total_amount'} =
1555 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1556 push @total_items, $total;
1559 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1562 foreach my $credit ( $self->_items_credits ) {
1564 $total->{'total_item'} = _latex_escape($credit->{'description'});
1566 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1567 push @total_items, $total;
1571 foreach my $payment ( $self->_items_payments ) {
1573 $total->{'total_item'} = _latex_escape($payment->{'description'});
1575 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1576 push @total_items, $total;
1581 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1582 $total->{'total_amount'} =
1583 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1584 push @total_items, $total;
1588 die "guru meditation #54";
1591 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1592 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1596 ) or die "can't open temp file: $!\n";
1597 if ( $format eq 'old' ) {
1598 print $fh join('', @filled_in );
1599 } elsif ( $format eq 'Text::Template' ) {
1600 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1602 die "guru meditation #32";
1606 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1611 =item print_ps [ TIME [ , TEMPLATE ] ]
1613 Returns an postscript invoice, as a scalar.
1615 TIME an optional value used to control the printing of overdue messages. The
1616 default is now. It isn't the date of the invoice; that's the `_date' field.
1617 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1618 L<Time::Local> and L<Date::Parse> for conversion functions.
1625 my $file = $self->print_latex(@_);
1627 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1630 my $sfile = shell_quote $file;
1632 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1633 or die "pslatex $file.tex failed; see $file.log for details?\n";
1634 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1635 or die "pslatex $file.tex failed; see $file.log for details?\n";
1637 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1638 or die "dvips failed";
1640 open(POSTSCRIPT, "<$file.ps")
1641 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1643 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1646 while (<POSTSCRIPT>) {
1656 =item print_pdf [ TIME [ , TEMPLATE ] ]
1658 Returns an PDF invoice, as a scalar.
1660 TIME an optional value used to control the printing of overdue messages. The
1661 default is now. It isn't the date of the invoice; that's the `_date' field.
1662 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1663 L<Time::Local> and L<Date::Parse> for conversion functions.
1670 my $file = $self->print_latex(@_);
1672 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1675 #system('pdflatex', "$file.tex");
1676 #system('pdflatex', "$file.tex");
1677 #! LaTeX Error: Unknown graphics extension: .eps.
1679 my $sfile = shell_quote $file;
1681 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1682 or die "pslatex $file.tex failed; see $file.log for details?\n";
1683 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1684 or die "pslatex $file.tex failed; see $file.log for details?\n";
1686 #system('dvipdf', "$file.dvi", "$file.pdf" );
1688 "dvips -q -t letter -f $sfile.dvi ".
1689 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1692 or die "dvips | gs failed: $!";
1694 open(PDF, "<$file.pdf")
1695 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1697 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1710 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1712 Returns an HTML invoice, as a scalar.
1714 TIME an optional value used to control the printing of overdue messages. The
1715 default is now. It isn't the date of the invoice; that's the `_date' field.
1716 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1717 L<Time::Local> and L<Date::Parse> for conversion functions.
1719 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1720 when emailing the invoice as part of a multipart/related MIME email.
1725 my( $self, $today, $template, $cid ) = @_;
1728 my $cust_main = $self->cust_main;
1729 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1730 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1732 $template ||= $self->_agent_template;
1733 my $templatefile = 'invoice_html';
1734 my $suffix = length($template) ? "_$template" : '';
1735 $templatefile .= $suffix;
1736 my @html_template = map "$_\n", $conf->config($templatefile)
1737 or die "cannot load config file $templatefile";
1739 my $html_template = new Text::Template(
1741 SOURCE => \@html_template,
1742 DELIMITERS => [ '<%=', '%>' ],
1745 $html_template->compile()
1746 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1748 my %invoice_data = (
1749 'invnum' => $self->invnum,
1750 'date' => time2str('%b %o, %Y', $self->_date),
1751 'today' => time2str('%b %o, %Y', $today),
1752 'agent' => encode_entities($cust_main->agent->agent),
1753 'payname' => encode_entities($cust_main->payname),
1754 'company' => encode_entities($cust_main->company),
1755 'address1' => encode_entities($cust_main->address1),
1756 'address2' => encode_entities($cust_main->address2),
1757 'city' => encode_entities($cust_main->city),
1758 'state' => encode_entities($cust_main->state),
1759 'zip' => encode_entities($cust_main->zip),
1760 'terms' => $conf->config('invoice_default_terms')
1761 || 'Payable upon receipt',
1763 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1766 $invoice_data{'returnaddress'} =
1767 length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1768 ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1771 s/\\\\\*?\s*$/<BR>/;
1772 s/\\hyphenation\{[\w\s\-]+\}//;
1775 $conf->config_orbase('invoice_latexreturnaddress', $template)
1778 my $countrydefault = $conf->config('countrydefault') || 'US';
1779 if ( $cust_main->country eq $countrydefault ) {
1780 $invoice_data{'country'} = '';
1782 $invoice_data{'country'} =
1783 encode_entities(code2country($cust_main->country));
1786 $invoice_data{'notes'} =
1787 length($conf->config_orbase('invoice_htmlnotes', $template))
1788 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1790 s/%%(.*)$/<!-- $1 -->/;
1791 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1792 s/\\begin\{enumerate\}/<ol>/;
1794 s/\\end\{enumerate\}/<\/ol>/;
1795 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1798 $conf->config_orbase('invoice_latexnotes', $template)
1801 # #do variable substitutions in notes
1802 # $invoice_data{'notes'} =
1804 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1805 # $conf->config_orbase('invoice_latexnotes', $suffix)
1808 $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1809 ? join("\n", $conf->config('invoice_htmlfooter') )
1810 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1811 $conf->config('invoice_latexfooter')
1814 $invoice_data{'po_line'} =
1815 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1816 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1819 my $money_char = $conf->config('money_char') || '$';
1821 foreach my $line_item ( $self->_items ) {
1823 ext_description => [],
1825 $detail->{'ref'} = $line_item->{'pkgnum'};
1826 $detail->{'description'} = encode_entities($line_item->{'description'});
1827 if ( exists $line_item->{'ext_description'} ) {
1828 @{$detail->{'ext_description'}} = map {
1829 encode_entities($_);
1830 } @{$line_item->{'ext_description'}};
1832 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1833 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1835 push @{$invoice_data{'detail_items'}}, $detail;
1840 foreach my $tax ( $self->_items_tax ) {
1842 $total->{'total_item'} = encode_entities($tax->{'description'});
1843 $taxtotal += $tax->{'amount'};
1844 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1845 push @{$invoice_data{'total_items'}}, $total;
1850 $total->{'total_item'} = 'Sub-total';
1851 $total->{'total_amount'} =
1852 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1853 unshift @{$invoice_data{'total_items'}}, $total;
1856 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1859 $total->{'total_item'} = '<b>Total</b>';
1860 $total->{'total_amount'} =
1861 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1862 push @{$invoice_data{'total_items'}}, $total;
1865 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1868 foreach my $credit ( $self->_items_credits ) {
1870 $total->{'total_item'} = encode_entities($credit->{'description'});
1872 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1873 push @{$invoice_data{'total_items'}}, $total;
1877 foreach my $payment ( $self->_items_payments ) {
1879 $total->{'total_item'} = encode_entities($payment->{'description'});
1881 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1882 push @{$invoice_data{'total_items'}}, $total;
1887 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1888 $total->{'total_amount'} =
1889 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1890 push @{$invoice_data{'total_items'}}, $total;
1893 $html_template->fill_in( HASH => \%invoice_data);
1896 # quick subroutine for print_latex
1898 # There are ten characters that LaTeX treats as special characters, which
1899 # means that they do not simply typeset themselves:
1900 # # $ % & ~ _ ^ \ { }
1902 # TeX ignores blanks following an escaped character; if you want a blank (as
1903 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1907 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1908 $value =~ s/([<>])/\$$1\$/g;
1912 #utility methods for print_*
1914 sub balance_due_msg {
1916 my $msg = 'Balance Due';
1917 return $msg unless $conf->exists('invoice_default_terms');
1918 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1919 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1920 } elsif ( $conf->config('invoice_default_terms') ) {
1921 $msg .= ' - '. $conf->config('invoice_default_terms');
1928 my @display = scalar(@_)
1930 : qw( _items_previous _items_pkg );
1931 #: qw( _items_pkg );
1932 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1934 foreach my $display ( @display ) {
1935 push @b, $self->$display(@_);
1940 sub _items_previous {
1942 my $cust_main = $self->cust_main;
1943 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1945 foreach ( @pr_cust_bill ) {
1947 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1948 ' ('. time2str('%x',$_->_date). ')',
1949 #'pkgpart' => 'N/A',
1951 'amount' => sprintf("%.2f", $_->owed),
1957 # 'description' => 'Previous Balance',
1958 # #'pkgpart' => 'N/A',
1959 # 'pkgnum' => 'N/A',
1960 # 'amount' => sprintf("%10.2f", $pr_total ),
1961 # 'ext_description' => [ map {
1962 # "Invoice ". $_->invnum.
1963 # " (". time2str("%x",$_->_date). ") ".
1964 # sprintf("%10.2f", $_->owed)
1965 # } @pr_cust_bill ],
1972 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1973 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1978 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1979 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1982 sub _items_cust_bill_pkg {
1984 my $cust_bill_pkg = shift;
1987 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1989 if ( $cust_bill_pkg->pkgnum > 0 ) {
1991 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1992 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1993 my $pkg = $part_pkg->pkg;
1995 if ( $cust_bill_pkg->setup != 0 ) {
1996 my $description = $pkg;
1997 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1998 my @d = $cust_pkg->h_labels_short($self->_date);
1999 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2001 description => $description,
2002 #pkgpart => $part_pkg->pkgpart,
2003 pkgnum => $cust_pkg->pkgnum,
2004 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2005 ext_description => \@d,
2009 if ( $cust_bill_pkg->recur != 0 ) {
2011 description => "$pkg (" .
2012 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2013 time2str('%x', $cust_bill_pkg->edate). ')',
2014 #pkgpart => $part_pkg->pkgpart,
2015 pkgnum => $cust_pkg->pkgnum,
2016 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2017 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2018 $cust_bill_pkg->sdate),
2019 $cust_bill_pkg->details,
2024 } else { #pkgnum tax or one-shot line item (??)
2026 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2027 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2029 if ( $cust_bill_pkg->setup != 0 ) {
2031 'description' => $itemdesc,
2032 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2035 if ( $cust_bill_pkg->recur != 0 ) {
2037 'description' => "$itemdesc (".
2038 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2039 time2str("%x", $cust_bill_pkg->edate). ')',
2040 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2052 sub _items_credits {
2057 foreach ( $self->cust_credited ) {
2059 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2061 my $reason = $_->cust_credit->reason;
2062 #my $reason = substr($_->cust_credit->reason,0,32);
2063 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2064 $reason = " ($reason) " if $reason;
2066 #'description' => 'Credit ref\#'. $_->crednum.
2067 # " (". time2str("%x",$_->cust_credit->_date) .")".
2069 'description' => 'Credit applied '.
2070 time2str("%x",$_->cust_credit->_date). $reason,
2071 'amount' => sprintf("%.2f",$_->amount),
2074 #foreach ( @cr_cust_credit ) {
2076 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2077 # $money_char. sprintf("%10.2f",$_->credited)
2085 sub _items_payments {
2089 #get & print payments
2090 foreach ( $self->cust_bill_pay ) {
2092 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2095 'description' => "Payment received ".
2096 time2str("%x",$_->cust_pay->_date ),
2097 'amount' => sprintf("%.2f", $_->amount )
2111 print_text formatting (and some logic :/) is in source, but needs to be
2112 slurped in from a file. Also number of lines ($=).
2116 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2117 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base