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 ( $conf->exists('invoice_latexreturnaddress')
1340 && length($conf->exists('invoice_latexreturnaddress'))
1343 $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1345 $returnaddress = '~';
1348 my %invoice_data = (
1349 'invnum' => $self->invnum,
1350 'date' => time2str('%b %o, %Y', $self->_date),
1351 'today' => time2str('%b %o, %Y', $today),
1352 'agent' => _latex_escape($cust_main->agent->agent),
1353 'payname' => _latex_escape($cust_main->payname),
1354 'company' => _latex_escape($cust_main->company),
1355 'address1' => _latex_escape($cust_main->address1),
1356 'address2' => _latex_escape($cust_main->address2),
1357 'city' => _latex_escape($cust_main->city),
1358 'state' => _latex_escape($cust_main->state),
1359 'zip' => _latex_escape($cust_main->zip),
1360 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1361 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1362 'returnaddress' => $returnaddress,
1364 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1365 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1366 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1369 my $countrydefault = $conf->config('countrydefault') || 'US';
1370 if ( $cust_main->country eq $countrydefault ) {
1371 $invoice_data{'country'} = '';
1373 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1376 $invoice_data{'notes'} =
1378 # #do variable substitutions in notes
1379 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1380 $conf->config_orbase('invoice_latexnotes', $template)
1382 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1385 $invoice_data{'footer'} =~ s/\n+$//;
1386 $invoice_data{'smallfooter'} =~ s/\n+$//;
1387 $invoice_data{'notes'} =~ s/\n+$//;
1389 $invoice_data{'po_line'} =
1390 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1391 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1395 if ( $format eq 'old' ) {
1398 my @total_item = ();
1399 while ( @invoice_template ) {
1400 my $line = shift @invoice_template;
1402 if ( $line =~ /^%%Detail\s*$/ ) {
1404 while ( ( my $line_item_line = shift @invoice_template )
1405 !~ /^%%EndDetail\s*$/ ) {
1406 push @line_item, $line_item_line;
1408 foreach my $line_item ( $self->_items ) {
1409 #foreach my $line_item ( $self->_items_pkg ) {
1410 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1411 $invoice_data{'description'} =
1412 _latex_escape($line_item->{'description'});
1413 if ( exists $line_item->{'ext_description'} ) {
1414 $invoice_data{'description'} .=
1415 "\\tabularnewline\n~~".
1416 join( "\\tabularnewline\n~~",
1417 map _latex_escape($_), @{$line_item->{'ext_description'}}
1420 $invoice_data{'amount'} = $line_item->{'amount'};
1421 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1423 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1426 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1428 while ( ( my $total_item_line = shift @invoice_template )
1429 !~ /^%%EndTotalDetails\s*$/ ) {
1430 push @total_item, $total_item_line;
1433 my @total_fill = ();
1436 foreach my $tax ( $self->_items_tax ) {
1437 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1438 $taxtotal += $tax->{'amount'};
1439 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1441 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1446 $invoice_data{'total_item'} = 'Sub-total';
1447 $invoice_data{'total_amount'} =
1448 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1449 unshift @total_fill,
1450 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1454 $invoice_data{'total_item'} = '\textbf{Total}';
1455 $invoice_data{'total_amount'} =
1456 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1458 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1461 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1464 foreach my $credit ( $self->_items_credits ) {
1465 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1467 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1469 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1474 foreach my $payment ( $self->_items_payments ) {
1475 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1477 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1479 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1483 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1484 $invoice_data{'total_amount'} =
1485 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1487 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1490 push @filled_in, @total_fill;
1493 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1494 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1495 push @filled_in, $line;
1506 } elsif ( $format eq 'Text::Template' ) {
1508 my @detail_items = ();
1509 my @total_items = ();
1511 $invoice_data{'detail_items'} = \@detail_items;
1512 $invoice_data{'total_items'} = \@total_items;
1514 foreach my $line_item ( $self->_items ) {
1516 ext_description => [],
1518 $detail->{'ref'} = $line_item->{'pkgnum'};
1519 $detail->{'quantity'} = 1;
1520 $detail->{'description'} = _latex_escape($line_item->{'description'});
1521 if ( exists $line_item->{'ext_description'} ) {
1522 @{$detail->{'ext_description'}} = map {
1524 } @{$line_item->{'ext_description'}};
1526 $detail->{'amount'} = $line_item->{'amount'};
1527 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1529 push @detail_items, $detail;
1534 foreach my $tax ( $self->_items_tax ) {
1536 $total->{'total_item'} = _latex_escape($tax->{'description'});
1537 $taxtotal += $tax->{'amount'};
1538 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1539 push @total_items, $total;
1544 $total->{'total_item'} = 'Sub-total';
1545 $total->{'total_amount'} =
1546 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1547 unshift @total_items, $total;
1552 $total->{'total_item'} = '\textbf{Total}';
1553 $total->{'total_amount'} =
1554 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1555 push @total_items, $total;
1558 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1561 foreach my $credit ( $self->_items_credits ) {
1563 $total->{'total_item'} = _latex_escape($credit->{'description'});
1565 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1566 push @total_items, $total;
1570 foreach my $payment ( $self->_items_payments ) {
1572 $total->{'total_item'} = _latex_escape($payment->{'description'});
1574 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1575 push @total_items, $total;
1580 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1581 $total->{'total_amount'} =
1582 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1583 push @total_items, $total;
1587 die "guru meditation #54";
1590 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1591 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1595 ) or die "can't open temp file: $!\n";
1596 if ( $format eq 'old' ) {
1597 print $fh join('', @filled_in );
1598 } elsif ( $format eq 'Text::Template' ) {
1599 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1601 die "guru meditation #32";
1605 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1610 =item print_ps [ TIME [ , TEMPLATE ] ]
1612 Returns an postscript invoice, as a scalar.
1614 TIME an optional value used to control the printing of overdue messages. The
1615 default is now. It isn't the date of the invoice; that's the `_date' field.
1616 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1617 L<Time::Local> and L<Date::Parse> for conversion functions.
1624 my $file = $self->print_latex(@_);
1626 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1629 my $sfile = shell_quote $file;
1631 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1632 or die "pslatex $file.tex failed; see $file.log for details?\n";
1633 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1634 or die "pslatex $file.tex failed; see $file.log for details?\n";
1636 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1637 or die "dvips failed";
1639 open(POSTSCRIPT, "<$file.ps")
1640 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1642 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1645 while (<POSTSCRIPT>) {
1655 =item print_pdf [ TIME [ , TEMPLATE ] ]
1657 Returns an PDF invoice, as a scalar.
1659 TIME an optional value used to control the printing of overdue messages. The
1660 default is now. It isn't the date of the invoice; that's the `_date' field.
1661 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1662 L<Time::Local> and L<Date::Parse> for conversion functions.
1669 my $file = $self->print_latex(@_);
1671 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1674 #system('pdflatex', "$file.tex");
1675 #system('pdflatex', "$file.tex");
1676 #! LaTeX Error: Unknown graphics extension: .eps.
1678 my $sfile = shell_quote $file;
1680 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1681 or die "pslatex $file.tex failed; see $file.log for details?\n";
1682 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1683 or die "pslatex $file.tex failed; see $file.log for details?\n";
1685 #system('dvipdf', "$file.dvi", "$file.pdf" );
1687 "dvips -q -t letter -f $sfile.dvi ".
1688 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1691 or die "dvips | gs failed: $!";
1693 open(PDF, "<$file.pdf")
1694 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1696 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1709 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1711 Returns an HTML invoice, as a scalar.
1713 TIME an optional value used to control the printing of overdue messages. The
1714 default is now. It isn't the date of the invoice; that's the `_date' field.
1715 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1716 L<Time::Local> and L<Date::Parse> for conversion functions.
1718 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1719 when emailing the invoice as part of a multipart/related MIME email.
1724 my( $self, $today, $template, $cid ) = @_;
1727 my $cust_main = $self->cust_main;
1728 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1729 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1731 $template ||= $self->_agent_template;
1732 my $templatefile = 'invoice_html';
1733 my $suffix = length($template) ? "_$template" : '';
1734 $templatefile .= $suffix;
1735 my @html_template = map "$_\n", $conf->config($templatefile)
1736 or die "cannot load config file $templatefile";
1738 my $html_template = new Text::Template(
1740 SOURCE => \@html_template,
1741 DELIMITERS => [ '<%=', '%>' ],
1744 $html_template->compile()
1745 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1747 my %invoice_data = (
1748 'invnum' => $self->invnum,
1749 'date' => time2str('%b %o, %Y', $self->_date),
1750 'today' => time2str('%b %o, %Y', $today),
1751 'agent' => encode_entities($cust_main->agent->agent),
1752 'payname' => encode_entities($cust_main->payname),
1753 'company' => encode_entities($cust_main->company),
1754 'address1' => encode_entities($cust_main->address1),
1755 'address2' => encode_entities($cust_main->address2),
1756 'city' => encode_entities($cust_main->city),
1757 'state' => encode_entities($cust_main->state),
1758 'zip' => encode_entities($cust_main->zip),
1759 'terms' => $conf->config('invoice_default_terms')
1760 || 'Payable upon receipt',
1762 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1765 $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
1766 ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1769 s/\\\\\*?\s*$/<BR>/;
1770 s/\\hyphenation\{[\w\s\-]+\}//;
1773 $conf->config('invoice_latexreturnaddress')
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