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.
555 INVOICE_FROM, if specified, overrides the default email invoice From: address.
561 my $template = scalar(@_) ? shift : '';
562 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
567 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
569 my @invoicing_list = $self->cust_main->invoicing_list;
571 $self->email($template, $invoice_from)
572 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
574 $self->print($template)
575 if grep { $_ eq 'POST' } @invoicing_list; #postal
577 $self->fax($template)
578 if grep { $_ eq 'FAX' } @invoicing_list; #fax
584 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
588 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
590 INVOICE_FROM, if specified, overrides the default email invoice From: address.
596 my $template = scalar(@_) ? shift : '';
600 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
602 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
603 $self->cust_main->invoicing_list;
605 #better to notify this person than silence
606 @invoicing_list = ($invoice_from) unless @invoicing_list;
608 my $error = send_email(
609 $self->generate_email(
610 'from' => $invoice_from,
611 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
612 'template' => $template,
615 die "can't email invoice: $error\n" if $error;
616 #die "$error\n" if $error;
620 =item lpr_data [ TEMPLATENAME ]
622 Returns the postscript or plaintext for this invoice.
624 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
629 my( $self, $template) = @_;
630 $conf->exists('invoice_latex')
631 ? [ $self->print_ps('', $template) ]
632 : [ $self->print_text('', $template) ];
635 =item print [ TEMPLATENAME ]
639 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
645 my $template = scalar(@_) ? shift : '';
647 my $lpr = $conf->config('lpr');
649 or die "Can't open pipe to $lpr: $!\n";
650 print LPR @{ $self->lpr_data($template) };
652 or die $! ? "Error closing $lpr: $!\n"
653 : "Exit status $? from $lpr\n";
656 =item fax [ TEMPLATENAME ]
660 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
666 my $template = scalar(@_) ? shift : '';
668 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
669 unless $conf->exists('invoice_latex');
671 my $dialstring = $self->cust_main->getfield('fax');
674 my $error = send_fax( 'docdata' => $self->lpr_data($template),
675 'dialstring' => $dialstring,
677 die $error if $error;
681 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
683 Like B<send>, but only sends the invoice if it is the newest open invoice for
693 grep { $_->owed > 0 }
694 qsearch('cust_bill', {
695 'custnum' => $self->custnum,
696 #'_date' => { op=>'>', value=>$self->_date },
697 'invnum' => { op=>'>', value=>$self->invnum },
704 =item send_csv OPTIONS
706 Sends invoice as a CSV data-file to a remote host with the specified protocol.
710 protocol - currently only "ftp"
716 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
717 and YYMMDDHHMMSS is a timestamp.
719 The fields of the CSV file is as follows:
721 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
725 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
727 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
728 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
729 fields are filled in.
731 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
732 first two fields (B<record_type> and B<invnum>) and the last five fields
733 (B<pkg> through B<edate>) are filled in.
735 =item invnum - invoice number
737 =item custnum - customer number
739 =item _date - invoice date
741 =item charged - total invoice amount
743 =item first - customer first name
745 =item last - customer first name
747 =item company - company name
749 =item address1 - address line 1
751 =item address2 - address line 1
761 =item pkg - line item description
763 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
765 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
767 =item sdate - start date for recurring fee
769 =item edate - end date for recurring fee
776 my($self, %opt) = @_;
778 #part one: create file
780 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
781 mkdir $spooldir, 0700 unless -d $spooldir;
783 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
785 open(CSV, ">$file") or die "can't open $file: $!";
787 eval "use Text::CSV_XS";
790 my $csv = Text::CSV_XS->new({'always_quote'=>1});
792 my $cust_main = $self->cust_main;
798 time2str("%x", $self->_date),
799 sprintf("%.2f", $self->charged),
800 ( map { $cust_main->getfield($_) }
801 qw( first last company address1 address2 city state zip country ) ),
803 ) or die "can't create csv";
804 print CSV $csv->string. "\n";
806 #new charges (false laziness w/print_text)
807 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
809 my($pkg, $setup, $recur, $sdate, $edate);
810 if ( $cust_bill_pkg->pkgnum ) {
812 ($pkg, $setup, $recur, $sdate, $edate) = (
813 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
814 ( $cust_bill_pkg->setup != 0
815 ? sprintf("%.2f", $cust_bill_pkg->setup )
817 ( $cust_bill_pkg->recur != 0
818 ? sprintf("%.2f", $cust_bill_pkg->recur )
820 time2str("%x", $cust_bill_pkg->sdate),
821 time2str("%x", $cust_bill_pkg->edate),
825 next unless $cust_bill_pkg->setup != 0;
826 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
827 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
829 ($pkg, $setup, $recur, $sdate, $edate) =
830 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
836 ( map { '' } (1..11) ),
837 ($pkg, $setup, $recur, $sdate, $edate)
838 ) or die "can't create csv";
839 print CSV $csv->string. "\n";
843 close CSV or die "can't close CSV: $!";
848 if ( $opt{protocol} eq 'ftp' ) {
849 eval "use Net::FTP;";
851 $net = Net::FTP->new($opt{server}) or die @$;
853 die "unknown protocol: $opt{protocol}";
856 $net->login( $opt{username}, $opt{password} )
857 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
859 $net->binary or die "can't set binary mode";
861 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
863 $net->put($file) or die "can't put $file: $!";
873 Pays this invoice with a compliemntary payment. If there is an error,
874 returns the error, otherwise returns false.
880 my $cust_pay = new FS::cust_pay ( {
881 'invnum' => $self->invnum,
882 'paid' => $self->owed,
885 'payinfo' => $self->cust_main->payinfo,
893 Attempts to pay this invoice with a credit card payment via a
894 Business::OnlinePayment realtime gateway. See
895 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
896 for supported processors.
902 $self->realtime_bop( 'CC', @_ );
907 Attempts to pay this invoice with an electronic check (ACH) payment via a
908 Business::OnlinePayment realtime gateway. See
909 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
910 for supported processors.
916 $self->realtime_bop( 'ECHECK', @_ );
921 Attempts to pay this invoice with phone bill (LEC) payment via a
922 Business::OnlinePayment realtime gateway. See
923 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
924 for supported processors.
930 $self->realtime_bop( 'LEC', @_ );
934 my( $self, $method ) = @_;
936 my $cust_main = $self->cust_main;
937 my $balance = $cust_main->balance;
938 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
939 $amount = sprintf("%.2f", $amount);
940 return "not run (balance $balance)" unless $amount > 0;
942 my $description = 'Internet Services';
943 if ( $conf->exists('business-onlinepayment-description') ) {
944 my $dtempl = $conf->config('business-onlinepayment-description');
946 my $agent_obj = $cust_main->agent
947 or die "can't retreive agent for $cust_main (agentnum ".
948 $cust_main->agentnum. ")";
949 my $agent = $agent_obj->agent;
950 my $pkgs = join(', ',
951 map { $_->cust_pkg->part_pkg->pkg }
952 grep { $_->pkgnum } $self->cust_bill_pkg
954 $description = eval qq("$dtempl");
957 $cust_main->realtime_bop($method, $amount,
958 'description' => $description,
959 'invnum' => $self->invnum,
966 Adds a payment for this invoice to the pending credit card batch (see
967 L<FS::cust_pay_batch>).
973 my $cust_main = $self->cust_main;
975 my $cust_pay_batch = new FS::cust_pay_batch ( {
976 'invnum' => $self->getfield('invnum'),
977 'custnum' => $cust_main->getfield('custnum'),
978 'last' => $cust_main->getfield('last'),
979 'first' => $cust_main->getfield('first'),
980 'address1' => $cust_main->getfield('address1'),
981 'address2' => $cust_main->getfield('address2'),
982 'city' => $cust_main->getfield('city'),
983 'state' => $cust_main->getfield('state'),
984 'zip' => $cust_main->getfield('zip'),
985 'country' => $cust_main->getfield('country'),
986 'cardnum' => $cust_main->payinfo,
987 'exp' => $cust_main->getfield('paydate'),
988 'payname' => $cust_main->getfield('payname'),
989 'amount' => $self->owed,
991 my $error = $cust_pay_batch->insert;
992 die $error if $error;
997 sub _agent_template {
999 $self->_agent_plandata('agent_templatename');
1002 sub _agent_invoice_from {
1004 $self->_agent_plandata('agent_invoice_from');
1007 sub _agent_plandata {
1008 my( $self, $option ) = @_;
1010 my $part_bill_event = qsearchs( 'part_bill_event',
1012 'payby' => $self->cust_main->payby,
1013 'plan' => 'send_agent',
1014 'plandata' => { 'op' => '~',
1015 'value' => "(^|\n)agentnum ".
1016 $self->cust_main->agentnum.
1021 'ORDER BY seconds LIMIT 1'
1024 return '' unless $part_bill_event;
1026 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1029 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1030 " plandata for $option";
1036 =item print_text [ TIME [ , TEMPLATE ] ]
1038 Returns an text invoice, as a list of lines.
1040 TIME an optional value used to control the printing of overdue messages. The
1041 default is now. It isn't the date of the invoice; that's the `_date' field.
1042 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1043 L<Time::Local> and L<Date::Parse> for conversion functions.
1047 #still some false laziness w/print_text
1050 my( $self, $today, $template ) = @_;
1053 # my $invnum = $self->invnum;
1054 my $cust_main = $self->cust_main;
1055 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1056 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1058 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1059 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1060 #my $balance_due = $self->owed + $pr_total - $cr_total;
1061 my $balance_due = $self->owed + $pr_total;
1064 #my($description,$amount);
1068 foreach ( @pr_cust_bill ) {
1070 "Previous Balance, Invoice #". $_->invnum.
1071 " (". time2str("%x",$_->_date). ")",
1072 $money_char. sprintf("%10.2f",$_->owed)
1075 if (@pr_cust_bill) {
1076 push @buf,['','-----------'];
1077 push @buf,[ 'Total Previous Balance',
1078 $money_char. sprintf("%10.2f",$pr_total ) ];
1083 foreach my $cust_bill_pkg (
1084 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1085 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1088 if ( $cust_bill_pkg->pkgnum > 0 ) {
1090 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1091 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1092 my $pkg = $part_pkg->pkg;
1094 if ( $cust_bill_pkg->setup != 0 ) {
1095 my $description = $pkg;
1096 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1097 push @buf, [ $description,
1098 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1100 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1101 $cust_pkg->h_labels($self->_date);
1104 if ( $cust_bill_pkg->recur != 0 ) {
1106 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1107 time2str("%x", $cust_bill_pkg->edate) . ")",
1108 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1111 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1112 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1115 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1117 } else { #pkgnum tax or one-shot line item
1118 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1119 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1121 if ( $cust_bill_pkg->setup != 0 ) {
1122 push @buf, [ $itemdesc,
1123 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1125 if ( $cust_bill_pkg->recur != 0 ) {
1126 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1127 . time2str("%x", $cust_bill_pkg->edate). ")",
1128 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1134 push @buf,['','-----------'];
1135 push @buf,['Total New Charges',
1136 $money_char. sprintf("%10.2f",$self->charged) ];
1139 push @buf,['','-----------'];
1140 push @buf,['Total Charges',
1141 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1145 foreach ( $self->cust_credited ) {
1147 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1149 my $reason = substr($_->cust_credit->reason,0,32);
1150 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1151 $reason = " ($reason) " if $reason;
1153 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1155 $money_char. sprintf("%10.2f",$_->amount)
1158 #foreach ( @cr_cust_credit ) {
1160 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1161 # $money_char. sprintf("%10.2f",$_->credited)
1165 #get & print payments
1166 foreach ( $self->cust_bill_pay ) {
1168 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1171 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1172 $money_char. sprintf("%10.2f",$_->amount )
1177 my $balance_due_msg = $self->balance_due_msg;
1179 push @buf,['','-----------'];
1180 push @buf,[$balance_due_msg, $money_char.
1181 sprintf("%10.2f", $balance_due ) ];
1183 #create the template
1184 $template ||= $self->_agent_template;
1185 my $templatefile = 'invoice_template';
1186 $templatefile .= "_$template" if length($template);
1187 my @invoice_template = $conf->config($templatefile)
1188 or die "cannot load config file $templatefile";
1191 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1192 /invoice_lines\((\d*)\)/;
1193 $invoice_lines += $1 || scalar(@buf);
1196 die "no invoice_lines() functions in template?" unless $wasfunc;
1197 my $invoice_template = new Text::Template (
1199 SOURCE => [ map "$_\n", @invoice_template ],
1200 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1201 $invoice_template->compile()
1202 or die "can't compile template: $Text::Template::ERROR";
1204 #setup template variables
1205 package FS::cust_bill::_template; #!
1206 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1208 $invnum = $self->invnum;
1209 $date = $self->_date;
1211 $agent = $self->cust_main->agent->agent;
1213 if ( $FS::cust_bill::invoice_lines ) {
1215 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1217 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1222 #format address (variable for the template)
1224 @address = ( '', '', '', '', '', '' );
1225 package FS::cust_bill; #!
1226 $FS::cust_bill::_template::address[$l++] =
1227 $cust_main->payname.
1228 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1229 ? " (P.O. #". $cust_main->payinfo. ")"
1233 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1234 if $cust_main->company;
1235 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1236 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1237 if $cust_main->address2;
1238 $FS::cust_bill::_template::address[$l++] =
1239 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1241 my $countrydefault = $conf->config('countrydefault') || 'US';
1242 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1243 unless $cust_main->country eq $countrydefault;
1245 # #overdue? (variable for the template)
1246 # $FS::cust_bill::_template::overdue = (
1248 # && $today > $self->_date
1249 ## && $self->printed > 1
1250 # && $self->printed > 0
1253 #and subroutine for the template
1254 sub FS::cust_bill::_template::invoice_lines {
1255 my $lines = shift || scalar(@buf);
1257 scalar(@buf) ? shift @buf : [ '', '' ];
1263 $FS::cust_bill::_template::page = 1;
1267 push @collect, split("\n",
1268 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1270 $FS::cust_bill::_template::page++;
1273 map "$_\n", @collect;
1277 =item print_latex [ TIME [ , TEMPLATE ] ]
1279 Internal method - returns a filename of a filled-in LaTeX template for this
1280 invoice (Note: add ".tex" to get the actual filename).
1282 See print_ps and print_pdf for methods that return PostScript and PDF output.
1284 TIME an optional value used to control the printing of overdue messages. The
1285 default is now. It isn't the date of the invoice; that's the `_date' field.
1286 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1287 L<Time::Local> and L<Date::Parse> for conversion functions.
1291 #still some false laziness w/print_text
1294 my( $self, $today, $template ) = @_;
1296 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1299 my $cust_main = $self->cust_main;
1300 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1301 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1303 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1304 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1305 #my $balance_due = $self->owed + $pr_total - $cr_total;
1306 my $balance_due = $self->owed + $pr_total;
1308 #create the template
1309 $template ||= $self->_agent_template;
1310 my $templatefile = 'invoice_latex';
1311 my $suffix = length($template) ? "_$template" : '';
1312 $templatefile .= $suffix;
1313 my @invoice_template = map "$_\n", $conf->config($templatefile)
1314 or die "cannot load config file $templatefile";
1316 my($format, $text_template);
1317 if ( grep { /^%%Detail/ } @invoice_template ) {
1318 #change this to a die when the old code is removed
1319 warn "old-style invoice template $templatefile; ".
1320 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1323 $format = 'Text::Template';
1324 $text_template = new Text::Template(
1326 SOURCE => \@invoice_template,
1327 DELIMITERS => [ '[@--', '--@]' ],
1330 $text_template->compile()
1331 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1335 if ( $conf->exists('invoice_latexreturnaddress')
1336 && length($conf->exists('invoice_latexreturnaddress'))
1339 $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1341 $returnaddress = '~';
1344 my %invoice_data = (
1345 'invnum' => $self->invnum,
1346 'date' => time2str('%b %o, %Y', $self->_date),
1347 'today' => time2str('%b %o, %Y', $today),
1348 'agent' => _latex_escape($cust_main->agent->agent),
1349 'payname' => _latex_escape($cust_main->payname),
1350 'company' => _latex_escape($cust_main->company),
1351 'address1' => _latex_escape($cust_main->address1),
1352 'address2' => _latex_escape($cust_main->address2),
1353 'city' => _latex_escape($cust_main->city),
1354 'state' => _latex_escape($cust_main->state),
1355 'zip' => _latex_escape($cust_main->zip),
1356 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1357 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1358 'returnaddress' => $returnaddress,
1360 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1361 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1362 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1365 my $countrydefault = $conf->config('countrydefault') || 'US';
1366 if ( $cust_main->country eq $countrydefault ) {
1367 $invoice_data{'country'} = '';
1369 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1372 $invoice_data{'notes'} =
1374 # #do variable substitutions in notes
1375 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1376 $conf->config_orbase('invoice_latexnotes', $template)
1378 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1381 $invoice_data{'footer'} =~ s/\n+$//;
1382 $invoice_data{'smallfooter'} =~ s/\n+$//;
1383 $invoice_data{'notes'} =~ s/\n+$//;
1385 $invoice_data{'po_line'} =
1386 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1387 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1391 if ( $format eq 'old' ) {
1394 my @total_item = ();
1395 while ( @invoice_template ) {
1396 my $line = shift @invoice_template;
1398 if ( $line =~ /^%%Detail\s*$/ ) {
1400 while ( ( my $line_item_line = shift @invoice_template )
1401 !~ /^%%EndDetail\s*$/ ) {
1402 push @line_item, $line_item_line;
1404 foreach my $line_item ( $self->_items ) {
1405 #foreach my $line_item ( $self->_items_pkg ) {
1406 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1407 $invoice_data{'description'} =
1408 _latex_escape($line_item->{'description'});
1409 if ( exists $line_item->{'ext_description'} ) {
1410 $invoice_data{'description'} .=
1411 "\\tabularnewline\n~~".
1412 join( "\\tabularnewline\n~~",
1413 map _latex_escape($_), @{$line_item->{'ext_description'}}
1416 $invoice_data{'amount'} = $line_item->{'amount'};
1417 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1419 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1422 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1424 while ( ( my $total_item_line = shift @invoice_template )
1425 !~ /^%%EndTotalDetails\s*$/ ) {
1426 push @total_item, $total_item_line;
1429 my @total_fill = ();
1432 foreach my $tax ( $self->_items_tax ) {
1433 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1434 $taxtotal += $tax->{'amount'};
1435 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1437 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1442 $invoice_data{'total_item'} = 'Sub-total';
1443 $invoice_data{'total_amount'} =
1444 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1445 unshift @total_fill,
1446 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1450 $invoice_data{'total_item'} = '\textbf{Total}';
1451 $invoice_data{'total_amount'} =
1452 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1454 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1457 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1460 foreach my $credit ( $self->_items_credits ) {
1461 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1463 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1465 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1470 foreach my $payment ( $self->_items_payments ) {
1471 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1473 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1475 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1479 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1480 $invoice_data{'total_amount'} =
1481 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1483 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1486 push @filled_in, @total_fill;
1489 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1490 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1491 push @filled_in, $line;
1502 } elsif ( $format eq 'Text::Template' ) {
1504 my @detail_items = ();
1505 my @total_items = ();
1507 $invoice_data{'detail_items'} = \@detail_items;
1508 $invoice_data{'total_items'} = \@total_items;
1510 foreach my $line_item ( $self->_items ) {
1512 ext_description => [],
1514 $detail->{'ref'} = $line_item->{'pkgnum'};
1515 $detail->{'quantity'} = 1;
1516 $detail->{'description'} = _latex_escape($line_item->{'description'});
1517 if ( exists $line_item->{'ext_description'} ) {
1518 @{$detail->{'ext_description'}} = map {
1520 } @{$line_item->{'ext_description'}};
1522 $detail->{'amount'} = $line_item->{'amount'};
1523 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1525 push @detail_items, $detail;
1530 foreach my $tax ( $self->_items_tax ) {
1532 $total->{'total_item'} = _latex_escape($tax->{'description'});
1533 $taxtotal += $tax->{'amount'};
1534 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1535 push @total_items, $total;
1540 $total->{'total_item'} = 'Sub-total';
1541 $total->{'total_amount'} =
1542 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1543 unshift @total_items, $total;
1548 $total->{'total_item'} = '\textbf{Total}';
1549 $total->{'total_amount'} =
1550 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1551 push @total_items, $total;
1554 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1557 foreach my $credit ( $self->_items_credits ) {
1559 $total->{'total_item'} = _latex_escape($credit->{'description'});
1561 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1562 push @total_items, $total;
1566 foreach my $payment ( $self->_items_payments ) {
1568 $total->{'total_item'} = _latex_escape($payment->{'description'});
1570 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1571 push @total_items, $total;
1576 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1577 $total->{'total_amount'} =
1578 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1579 push @total_items, $total;
1583 die "guru meditation #54";
1586 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1587 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1591 ) or die "can't open temp file: $!\n";
1592 if ( $format eq 'old' ) {
1593 print $fh join('', @filled_in );
1594 } elsif ( $format eq 'Text::Template' ) {
1595 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1597 die "guru meditation #32";
1601 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1606 =item print_ps [ TIME [ , TEMPLATE ] ]
1608 Returns an postscript invoice, as a scalar.
1610 TIME an optional value used to control the printing of overdue messages. The
1611 default is now. It isn't the date of the invoice; that's the `_date' field.
1612 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1613 L<Time::Local> and L<Date::Parse> for conversion functions.
1620 my $file = $self->print_latex(@_);
1622 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1625 my $sfile = shell_quote $file;
1627 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1628 or die "pslatex $file.tex failed; see $file.log for details?\n";
1629 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1630 or die "pslatex $file.tex failed; see $file.log for details?\n";
1632 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1633 or die "dvips failed";
1635 open(POSTSCRIPT, "<$file.ps")
1636 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1638 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1641 while (<POSTSCRIPT>) {
1651 =item print_pdf [ TIME [ , TEMPLATE ] ]
1653 Returns an PDF invoice, as a scalar.
1655 TIME an optional value used to control the printing of overdue messages. The
1656 default is now. It isn't the date of the invoice; that's the `_date' field.
1657 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1658 L<Time::Local> and L<Date::Parse> for conversion functions.
1665 my $file = $self->print_latex(@_);
1667 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1670 #system('pdflatex', "$file.tex");
1671 #system('pdflatex', "$file.tex");
1672 #! LaTeX Error: Unknown graphics extension: .eps.
1674 my $sfile = shell_quote $file;
1676 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1677 or die "pslatex $file.tex failed; see $file.log for details?\n";
1678 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1679 or die "pslatex $file.tex failed; see $file.log for details?\n";
1681 #system('dvipdf', "$file.dvi", "$file.pdf" );
1683 "dvips -q -t letter -f $sfile.dvi ".
1684 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1687 or die "dvips | gs failed: $!";
1689 open(PDF, "<$file.pdf")
1690 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1692 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1705 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1707 Returns an HTML invoice, as a scalar.
1709 TIME an optional value used to control the printing of overdue messages. The
1710 default is now. It isn't the date of the invoice; that's the `_date' field.
1711 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1712 L<Time::Local> and L<Date::Parse> for conversion functions.
1714 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1715 when emailing the invoice as part of a multipart/related MIME email.
1720 my( $self, $today, $template, $cid ) = @_;
1723 my $cust_main = $self->cust_main;
1724 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1725 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1727 $template ||= $self->_agent_template;
1728 my $templatefile = 'invoice_html';
1729 my $suffix = length($template) ? "_$template" : '';
1730 $templatefile .= $suffix;
1731 my @html_template = map "$_\n", $conf->config($templatefile)
1732 or die "cannot load config file $templatefile";
1734 my $html_template = new Text::Template(
1736 SOURCE => \@html_template,
1737 DELIMITERS => [ '<%=', '%>' ],
1740 $html_template->compile()
1741 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1743 my %invoice_data = (
1744 'invnum' => $self->invnum,
1745 'date' => time2str('%b %o, %Y', $self->_date),
1746 'today' => time2str('%b %o, %Y', $today),
1747 'agent' => encode_entities($cust_main->agent->agent),
1748 'payname' => encode_entities($cust_main->payname),
1749 'company' => encode_entities($cust_main->company),
1750 'address1' => encode_entities($cust_main->address1),
1751 'address2' => encode_entities($cust_main->address2),
1752 'city' => encode_entities($cust_main->city),
1753 'state' => encode_entities($cust_main->state),
1754 'zip' => encode_entities($cust_main->zip),
1755 'terms' => $conf->config('invoice_default_terms')
1756 || 'Payable upon receipt',
1758 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1761 $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
1762 ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1765 s/\\\\\*?\s*$/<BR>/;
1766 s/\\hyphenation\{[\w\s\-]+\}//;
1769 $conf->config('invoice_latexreturnaddress')
1772 my $countrydefault = $conf->config('countrydefault') || 'US';
1773 if ( $cust_main->country eq $countrydefault ) {
1774 $invoice_data{'country'} = '';
1776 $invoice_data{'country'} =
1777 encode_entities(code2country($cust_main->country));
1780 $invoice_data{'notes'} =
1781 length($conf->config_orbase('invoice_htmlnotes', $template))
1782 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1784 s/%%(.*)$/<!-- $1 -->/;
1785 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1786 s/\\begin\{enumerate\}/<ol>/;
1788 s/\\end\{enumerate\}/<\/ol>/;
1789 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1792 $conf->config_orbase('invoice_latexnotes', $template)
1795 # #do variable substitutions in notes
1796 # $invoice_data{'notes'} =
1798 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1799 # $conf->config_orbase('invoice_latexnotes', $suffix)
1802 $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1803 ? join("\n", $conf->config('invoice_htmlfooter') )
1804 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1805 $conf->config('invoice_latexfooter')
1808 $invoice_data{'po_line'} =
1809 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1810 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1813 my $money_char = $conf->config('money_char') || '$';
1815 foreach my $line_item ( $self->_items ) {
1817 ext_description => [],
1819 $detail->{'ref'} = $line_item->{'pkgnum'};
1820 $detail->{'description'} = encode_entities($line_item->{'description'});
1821 if ( exists $line_item->{'ext_description'} ) {
1822 @{$detail->{'ext_description'}} = map {
1823 encode_entities($_);
1824 } @{$line_item->{'ext_description'}};
1826 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1827 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1829 push @{$invoice_data{'detail_items'}}, $detail;
1834 foreach my $tax ( $self->_items_tax ) {
1836 $total->{'total_item'} = encode_entities($tax->{'description'});
1837 $taxtotal += $tax->{'amount'};
1838 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1839 push @{$invoice_data{'total_items'}}, $total;
1844 $total->{'total_item'} = 'Sub-total';
1845 $total->{'total_amount'} =
1846 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1847 unshift @{$invoice_data{'total_items'}}, $total;
1850 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1853 $total->{'total_item'} = '<b>Total</b>';
1854 $total->{'total_amount'} =
1855 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1856 push @{$invoice_data{'total_items'}}, $total;
1859 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1862 foreach my $credit ( $self->_items_credits ) {
1864 $total->{'total_item'} = encode_entities($credit->{'description'});
1866 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1867 push @{$invoice_data{'total_items'}}, $total;
1871 foreach my $payment ( $self->_items_payments ) {
1873 $total->{'total_item'} = encode_entities($payment->{'description'});
1875 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1876 push @{$invoice_data{'total_items'}}, $total;
1881 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1882 $total->{'total_amount'} =
1883 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1884 push @{$invoice_data{'total_items'}}, $total;
1887 $html_template->fill_in( HASH => \%invoice_data);
1890 # quick subroutine for print_latex
1892 # There are ten characters that LaTeX treats as special characters, which
1893 # means that they do not simply typeset themselves:
1894 # # $ % & ~ _ ^ \ { }
1896 # TeX ignores blanks following an escaped character; if you want a blank (as
1897 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1901 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1902 $value =~ s/([<>])/\$$1\$/g;
1906 #utility methods for print_*
1908 sub balance_due_msg {
1910 my $msg = 'Balance Due';
1911 return $msg unless $conf->exists('invoice_default_terms');
1912 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1913 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1914 } elsif ( $conf->config('invoice_default_terms') ) {
1915 $msg .= ' - '. $conf->config('invoice_default_terms');
1922 my @display = scalar(@_)
1924 : qw( _items_previous _items_pkg );
1925 #: qw( _items_pkg );
1926 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1928 foreach my $display ( @display ) {
1929 push @b, $self->$display(@_);
1934 sub _items_previous {
1936 my $cust_main = $self->cust_main;
1937 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1939 foreach ( @pr_cust_bill ) {
1941 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1942 ' ('. time2str('%x',$_->_date). ')',
1943 #'pkgpart' => 'N/A',
1945 'amount' => sprintf("%.2f", $_->owed),
1951 # 'description' => 'Previous Balance',
1952 # #'pkgpart' => 'N/A',
1953 # 'pkgnum' => 'N/A',
1954 # 'amount' => sprintf("%10.2f", $pr_total ),
1955 # 'ext_description' => [ map {
1956 # "Invoice ". $_->invnum.
1957 # " (". time2str("%x",$_->_date). ") ".
1958 # sprintf("%10.2f", $_->owed)
1959 # } @pr_cust_bill ],
1966 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1967 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1972 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1973 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1976 sub _items_cust_bill_pkg {
1978 my $cust_bill_pkg = shift;
1981 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1983 if ( $cust_bill_pkg->pkgnum > 0 ) {
1985 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1986 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1987 my $pkg = $part_pkg->pkg;
1989 if ( $cust_bill_pkg->setup != 0 ) {
1990 my $description = $pkg;
1991 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1992 my @d = $cust_pkg->h_labels_short($self->_date);
1993 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1995 description => $description,
1996 #pkgpart => $part_pkg->pkgpart,
1997 pkgnum => $cust_pkg->pkgnum,
1998 amount => sprintf("%.2f", $cust_bill_pkg->setup),
1999 ext_description => \@d,
2003 if ( $cust_bill_pkg->recur != 0 ) {
2005 description => "$pkg (" .
2006 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2007 time2str('%x', $cust_bill_pkg->edate). ')',
2008 #pkgpart => $part_pkg->pkgpart,
2009 pkgnum => $cust_pkg->pkgnum,
2010 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2011 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2012 $cust_bill_pkg->sdate),
2013 $cust_bill_pkg->details,
2018 } else { #pkgnum tax or one-shot line item (??)
2020 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2021 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2023 if ( $cust_bill_pkg->setup != 0 ) {
2025 'description' => $itemdesc,
2026 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2029 if ( $cust_bill_pkg->recur != 0 ) {
2031 'description' => "$itemdesc (".
2032 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2033 time2str("%x", $cust_bill_pkg->edate). ')',
2034 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2046 sub _items_credits {
2051 foreach ( $self->cust_credited ) {
2053 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2055 my $reason = $_->cust_credit->reason;
2056 #my $reason = substr($_->cust_credit->reason,0,32);
2057 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2058 $reason = " ($reason) " if $reason;
2060 #'description' => 'Credit ref\#'. $_->crednum.
2061 # " (". time2str("%x",$_->cust_credit->_date) .")".
2063 'description' => 'Credit applied '.
2064 time2str("%x",$_->cust_credit->_date). $reason,
2065 'amount' => sprintf("%.2f",$_->amount),
2068 #foreach ( @cr_cust_credit ) {
2070 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2071 # $money_char. sprintf("%10.2f",$_->credited)
2079 sub _items_payments {
2083 #get & print payments
2084 foreach ( $self->cust_bill_pay ) {
2086 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2089 'description' => "Payment received ".
2090 time2str("%x",$_->cust_pay->_date ),
2091 'amount' => sprintf("%.2f", $_->amount )
2105 print_text formatting (and some logic :/) is in source, but needs to be
2106 slurped in from a file. Also number of lines ($=).
2110 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2111 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base