4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
7 use Text::Template 1.20;
9 use String::ShellQuote;
11 use FS::UID qw( datasrc );
12 use FS::Record qw( qsearch qsearchs );
13 use FS::Misc qw( send_email send_fax );
15 use FS::cust_bill_pkg;
19 use FS::cust_credit_bill;
20 use FS::cust_pay_batch;
21 use FS::cust_bill_event;
23 @ISA = qw( FS::Record );
25 #ask FS::UID to run this stuff for us later
26 FS::UID->install_callback( sub {
28 $money_char = $conf->config('money_char') || '$';
33 FS::cust_bill - Object methods for cust_bill records
39 $record = new FS::cust_bill \%hash;
40 $record = new FS::cust_bill { 'column' => 'value' };
42 $error = $record->insert;
44 $error = $new_record->replace($old_record);
46 $error = $record->delete;
48 $error = $record->check;
50 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
52 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
54 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
56 @cust_pay_objects = $cust_bill->cust_pay;
58 $tax_amount = $record->tax;
60 @lines = $cust_bill->print_text;
61 @lines = $cust_bill->print_text $time;
65 An FS::cust_bill object represents an invoice; a declaration that a customer
66 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
67 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
68 following fields are currently supported:
72 =item invnum - primary key (assigned automatically for new invoices)
74 =item custnum - customer (see L<FS::cust_main>)
76 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
77 L<Time::Local> and L<Date::Parse> for conversion functions.
79 =item charged - amount of this invoice
81 =item printed - deprecated
83 =item closed - books closed flag, empty or `Y'
93 Creates a new invoice. To add the invoice to the database, see L<"insert">.
94 Invoices are normally created by calling the bill method of a customer object
95 (see L<FS::cust_main>).
99 sub table { 'cust_bill'; }
103 Adds this invoice to the database ("Posts" the invoice). If there is an error,
104 returns the error, otherwise returns false.
108 Currently unimplemented. I don't remove invoices because there would then be
109 no record you ever posted this invoice (which is bad, no?)
115 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
116 $self->SUPER::delete(@_);
119 =item replace OLD_RECORD
121 Replaces the OLD_RECORD with this one in the database. If there is an error,
122 returns the error, otherwise returns false.
124 Only printed may be changed. printed is normally updated by calling the
125 collect method of a customer object (see L<FS::cust_main>).
130 my( $new, $old ) = ( shift, shift );
131 return "Can't change custnum!" unless $old->custnum == $new->custnum;
132 #return "Can't change _date!" unless $old->_date eq $new->_date;
133 return "Can't change _date!" unless $old->_date == $new->_date;
134 return "Can't change charged!" unless $old->charged == $new->charged;
136 $new->SUPER::replace($old);
141 Checks all fields to make sure this is a valid invoice. If there is an error,
142 returns the error, otherwise returns false. Called by the insert and replace
151 $self->ut_numbern('invnum')
152 || $self->ut_number('custnum')
153 || $self->ut_numbern('_date')
154 || $self->ut_money('charged')
155 || $self->ut_numbern('printed')
156 || $self->ut_enum('closed', [ '', 'Y' ])
158 return $error if $error;
160 return "Unknown customer"
161 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
163 $self->_date(time) unless $self->_date;
165 $self->printed(0) if $self->printed eq '';
172 Returns a list consisting of the total previous balance for this customer,
173 followed by the previous outstanding invoices (as FS::cust_bill objects also).
180 my @cust_bill = sort { $a->_date <=> $b->_date }
181 grep { $_->owed != 0 && $_->_date < $self->_date }
182 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
184 foreach ( @cust_bill ) { $total += $_->owed; }
190 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
196 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
199 =item cust_bill_event
201 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
206 sub cust_bill_event {
208 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
214 Returns the customer (see L<FS::cust_main>) for this invoice.
220 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
225 Depreciated. See the cust_credited method.
227 #Returns a list consisting of the total previous credited (see
228 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
229 #outstanding credits (FS::cust_credit objects).
235 croak "FS::cust_bill->cust_credit depreciated; see ".
236 "FS::cust_bill->cust_credit_bill";
239 #my @cust_credit = sort { $a->_date <=> $b->_date }
240 # grep { $_->credited != 0 && $_->_date < $self->_date }
241 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
243 #foreach (@cust_credit) { $total += $_->credited; }
244 #$total, @cust_credit;
249 Depreciated. See the cust_bill_pay method.
251 #Returns all payments (see L<FS::cust_pay>) for this invoice.
257 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
259 #sort { $a->_date <=> $b->_date }
260 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
266 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
272 sort { $a->_date <=> $b->_date }
273 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
278 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
284 sort { $a->_date <=> $b->_date }
285 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
291 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
298 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
300 foreach (@taxlines) { $total += $_->setup; }
306 Returns the amount owed (still outstanding) on this invoice, which is charged
307 minus all payment applications (see L<FS::cust_bill_pay>) and credit
308 applications (see L<FS::cust_credit_bill>).
314 my $balance = $self->charged;
315 $balance -= $_->amount foreach ( $self->cust_bill_pay );
316 $balance -= $_->amount foreach ( $self->cust_credited );
317 $balance = sprintf( "%.2f", $balance);
318 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
323 =item generate_email PARAMHASH
325 PARAMHASH can contain the following:
329 =item from => sender address, required
331 =item tempate => alternate template name, optional
333 =item print_text => text attachment arrayref, optional
335 =item subject => email subject, optional
339 Returns an argument list to be passed to L<FS::Misc::send_email>.
349 if ($conf->exists('invoice_email_pdf')) {
350 #warn "[FS::cust_bill::send] creating PDF attachment";
351 #mime parts arguments a la MIME::Entity->build().
354 'Type' => 'application/pdf',
355 'Encoding' => 'base64',
356 'Data' => [ $self->print_pdf('', $args{'template'}) ],
357 'Disposition' => 'attachment',
358 'Filename' => 'invoice.pdf',
364 if ($conf->exists('invoice_email_pdf')
365 and scalar($conf->config('invoice_email_pdf_note'))) {
367 #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'";
368 $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ];
370 #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'";
371 if (ref($args{'print_text'}) eq 'ARRAY') {
372 $email_text = $args{'print_text'};
374 $email_text = [ $self->print_text('', $args{'template'}) ];
379 if (ref($args{'to'} eq 'ARRAY')) {
380 @invoicing_list = @{$args{'to'}};
382 @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
386 'from' => $args{'from'},
387 'to' => [ @invoicing_list ],
388 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
389 'body' => $email_text,
390 'mimeparts' => $mimeparts,
396 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
398 Sends this invoice to the destinations configured for this customer: send
399 emails or print. See L<FS::cust_main_invoice>.
401 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
403 AGENTNUM, if specified, means that this invoice will only be sent for customers
404 of the specified agent.
406 INVOICE_FROM, if specified, overrides the default email invoice From: address.
412 my $template = scalar(@_) ? shift : '';
413 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
417 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
419 my @print_text = $self->print_text('', $template);
420 my @invoicing_list = $self->cust_main->invoicing_list;
422 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) {
425 #better to notify this person than silence
426 @invoicing_list = ($invoice_from) unless @invoicing_list;
428 my $error = send_email(
429 $self->generate_email(
430 'from' => $invoice_from,
431 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
432 'print_text' => [ @print_text ],
435 die "can't email invoice: $error\n" if $error;
436 #die "$error\n" if $error;
440 if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
442 if ($conf->config('invoice_latex')) {
443 $lpr_data = [ $self->print_ps('', $template) ];
445 $lpr_data = \@print_text;
448 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
449 my $lpr = $conf->config('lpr');
451 or die "Can't open pipe to $lpr: $!\n";
452 print LPR @{$lpr_data};
454 or die $! ? "Error closing $lpr: $!\n"
455 : "Exit status $? from $lpr\n";
458 if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
459 die 'FAX invoice destination not supported with plain text invoices.'
460 unless $conf->exists('invoice_latex');
461 my $dialstring = $self->cust_main->getfield('fax');
463 my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
464 die $error if $error;
473 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
475 Like B<send>, but only sends the invoice if it is the newest open invoice for
485 grep { $_->owed > 0 }
486 qsearch('cust_bill', {
487 'custnum' => $self->custnum,
488 #'_date' => { op=>'>', value=>$self->_date },
489 'invnum' => { op=>'>', value=>$self->invnum },
496 =item send_csv OPTIONS
498 Sends invoice as a CSV data-file to a remote host with the specified protocol.
502 protocol - currently only "ftp"
508 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
509 and YYMMDDHHMMSS is a timestamp.
511 The fields of the CSV file is as follows:
513 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
517 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
519 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
520 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
521 fields are filled in.
523 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
524 first two fields (B<record_type> and B<invnum>) and the last five fields
525 (B<pkg> through B<edate>) are filled in.
527 =item invnum - invoice number
529 =item custnum - customer number
531 =item _date - invoice date
533 =item charged - total invoice amount
535 =item first - customer first name
537 =item last - customer first name
539 =item company - company name
541 =item address1 - address line 1
543 =item address2 - address line 1
553 =item pkg - line item description
555 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
557 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
559 =item sdate - start date for recurring fee
561 =item edate - end date for recurring fee
568 my($self, %opt) = @_;
570 #part one: create file
572 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
573 mkdir $spooldir, 0700 unless -d $spooldir;
575 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
577 open(CSV, ">$file") or die "can't open $file: $!";
579 eval "use Text::CSV_XS";
582 my $csv = Text::CSV_XS->new({'always_quote'=>1});
584 my $cust_main = $self->cust_main;
590 time2str("%x", $self->_date),
591 sprintf("%.2f", $self->charged),
592 ( map { $cust_main->getfield($_) }
593 qw( first last company address1 address2 city state zip country ) ),
595 ) or die "can't create csv";
596 print CSV $csv->string. "\n";
598 #new charges (false laziness w/print_text)
599 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
601 my($pkg, $setup, $recur, $sdate, $edate);
602 if ( $cust_bill_pkg->pkgnum ) {
604 ($pkg, $setup, $recur, $sdate, $edate) = (
605 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
606 ( $cust_bill_pkg->setup != 0
607 ? sprintf("%.2f", $cust_bill_pkg->setup )
609 ( $cust_bill_pkg->recur != 0
610 ? sprintf("%.2f", $cust_bill_pkg->recur )
612 time2str("%x", $cust_bill_pkg->sdate),
613 time2str("%x", $cust_bill_pkg->edate),
617 next unless $cust_bill_pkg->setup != 0;
618 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
619 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
621 ($pkg, $setup, $recur, $sdate, $edate) =
622 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
628 ( map { '' } (1..11) ),
629 ($pkg, $setup, $recur, $sdate, $edate)
630 ) or die "can't create csv";
631 print CSV $csv->string. "\n";
635 close CSV or die "can't close CSV: $!";
640 if ( $opt{protocol} eq 'ftp' ) {
641 eval "use Net::FTP;";
643 $net = Net::FTP->new($opt{server}) or die @$;
645 die "unknown protocol: $opt{protocol}";
648 $net->login( $opt{username}, $opt{password} )
649 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
651 $net->binary or die "can't set binary mode";
653 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
655 $net->put($file) or die "can't put $file: $!";
665 Pays this invoice with a compliemntary payment. If there is an error,
666 returns the error, otherwise returns false.
672 my $cust_pay = new FS::cust_pay ( {
673 'invnum' => $self->invnum,
674 'paid' => $self->owed,
677 'payinfo' => $self->cust_main->payinfo,
685 Attempts to pay this invoice with a credit card payment via a
686 Business::OnlinePayment realtime gateway. See
687 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
688 for supported processors.
694 $self->realtime_bop( 'CC', @_ );
699 Attempts to pay this invoice with an electronic check (ACH) payment via a
700 Business::OnlinePayment realtime gateway. See
701 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
702 for supported processors.
708 $self->realtime_bop( 'ECHECK', @_ );
713 Attempts to pay this invoice with phone bill (LEC) payment via a
714 Business::OnlinePayment realtime gateway. See
715 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
716 for supported processors.
722 $self->realtime_bop( 'LEC', @_ );
726 my( $self, $method ) = @_;
728 my $cust_main = $self->cust_main;
729 my $balance = $cust_main->balance;
730 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
731 $amount = sprintf("%.2f", $amount);
732 return "not run (balance $balance)" unless $amount > 0;
734 my $description = 'Internet Services';
735 if ( $conf->exists('business-onlinepayment-description') ) {
736 my $dtempl = $conf->config('business-onlinepayment-description');
738 my $agent_obj = $cust_main->agent
739 or die "can't retreive agent for $cust_main (agentnum ".
740 $cust_main->agentnum. ")";
741 my $agent = $agent_obj->agent;
742 my $pkgs = join(', ',
743 map { $_->cust_pkg->part_pkg->pkg }
744 grep { $_->pkgnum } $self->cust_bill_pkg
746 $description = eval qq("$dtempl");
749 $cust_main->realtime_bop($method, $amount,
750 'description' => $description,
751 'invnum' => $self->invnum,
758 Adds a payment for this invoice to the pending credit card batch (see
759 L<FS::cust_pay_batch>).
765 my $cust_main = $self->cust_main;
767 my $cust_pay_batch = new FS::cust_pay_batch ( {
768 'invnum' => $self->getfield('invnum'),
769 'custnum' => $cust_main->getfield('custnum'),
770 'last' => $cust_main->getfield('last'),
771 'first' => $cust_main->getfield('first'),
772 'address1' => $cust_main->getfield('address1'),
773 'address2' => $cust_main->getfield('address2'),
774 'city' => $cust_main->getfield('city'),
775 'state' => $cust_main->getfield('state'),
776 'zip' => $cust_main->getfield('zip'),
777 'country' => $cust_main->getfield('country'),
778 'cardnum' => $cust_main->payinfo,
779 'exp' => $cust_main->getfield('paydate'),
780 'payname' => $cust_main->getfield('payname'),
781 'amount' => $self->owed,
783 my $error = $cust_pay_batch->insert;
784 die $error if $error;
789 sub _agent_template {
791 $self->_agent_plandata('agent_templatename');
794 sub _agent_invoice_from {
796 $self->_agent_plandata('agent_invoice_from');
799 sub _agent_plandata {
800 my( $self, $option ) = @_;
802 my $part_bill_event = qsearchs( 'part_bill_event',
804 'payby' => $self->cust_main->payby,
805 'plan' => 'send_agent',
806 'plandata' => { 'op' => '~',
807 'value' => "(^|\n)agentnum ".
808 $self->cust_main->agentnum.
813 'ORDER BY seconds LIMIT 1'
816 return '' unless $part_bill_event;
818 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
821 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
822 " plandata for $option";
828 =item print_text [ TIME [ , TEMPLATE ] ]
830 Returns an text invoice, as a list of lines.
832 TIME an optional value used to control the printing of overdue messages. The
833 default is now. It isn't the date of the invoice; that's the `_date' field.
834 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
835 L<Time::Local> and L<Date::Parse> for conversion functions.
839 #still some false laziness w/print_text
842 my( $self, $today, $template ) = @_;
845 # my $invnum = $self->invnum;
846 my $cust_main = $self->cust_main;
847 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
848 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
850 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
851 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
852 #my $balance_due = $self->owed + $pr_total - $cr_total;
853 my $balance_due = $self->owed + $pr_total;
856 #my($description,$amount);
860 foreach ( @pr_cust_bill ) {
862 "Previous Balance, Invoice #". $_->invnum.
863 " (". time2str("%x",$_->_date). ")",
864 $money_char. sprintf("%10.2f",$_->owed)
868 push @buf,['','-----------'];
869 push @buf,[ 'Total Previous Balance',
870 $money_char. sprintf("%10.2f",$pr_total ) ];
875 foreach my $cust_bill_pkg (
876 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
877 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
880 if ( $cust_bill_pkg->pkgnum ) {
882 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
883 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
884 my $pkg = $part_pkg->pkg;
886 if ( $cust_bill_pkg->setup != 0 ) {
887 my $description = $pkg;
888 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
889 push @buf, [ $description,
890 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
892 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
893 $cust_pkg->h_labels($self->_date);
896 if ( $cust_bill_pkg->recur != 0 ) {
898 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
899 time2str("%x", $cust_bill_pkg->edate) . ")",
900 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
903 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
904 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
907 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
909 } else { #pkgnum tax or one-shot line item
910 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
911 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
913 if ( $cust_bill_pkg->setup != 0 ) {
914 push @buf, [ $itemdesc,
915 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
917 if ( $cust_bill_pkg->recur != 0 ) {
918 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
919 . time2str("%x", $cust_bill_pkg->edate). ")",
920 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
926 push @buf,['','-----------'];
927 push @buf,['Total New Charges',
928 $money_char. sprintf("%10.2f",$self->charged) ];
931 push @buf,['','-----------'];
932 push @buf,['Total Charges',
933 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
937 foreach ( $self->cust_credited ) {
939 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
941 my $reason = substr($_->cust_credit->reason,0,32);
942 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
943 $reason = " ($reason) " if $reason;
945 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
947 $money_char. sprintf("%10.2f",$_->amount)
950 #foreach ( @cr_cust_credit ) {
952 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
953 # $money_char. sprintf("%10.2f",$_->credited)
957 #get & print payments
958 foreach ( $self->cust_bill_pay ) {
960 #something more elaborate if $_->amount ne ->cust_pay->paid ?
963 "Payment received ". time2str("%x",$_->cust_pay->_date ),
964 $money_char. sprintf("%10.2f",$_->amount )
969 my $balance_due_msg = $self->balance_due_msg;
971 push @buf,['','-----------'];
972 push @buf,[$balance_due_msg, $money_char.
973 sprintf("%10.2f", $balance_due ) ];
976 $template ||= $self->_agent_template;
977 my $templatefile = 'invoice_template';
978 $templatefile .= "_$template" if length($template);
979 my @invoice_template = $conf->config($templatefile)
980 or die "cannot load config file $templatefile";
983 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
984 /invoice_lines\((\d*)\)/;
985 $invoice_lines += $1 || scalar(@buf);
988 die "no invoice_lines() functions in template?" unless $wasfunc;
989 my $invoice_template = new Text::Template (
991 SOURCE => [ map "$_\n", @invoice_template ],
992 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
993 $invoice_template->compile()
994 or die "can't compile template: $Text::Template::ERROR";
996 #setup template variables
997 package FS::cust_bill::_template; #!
998 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1000 $invnum = $self->invnum;
1001 $date = $self->_date;
1003 $agent = $self->cust_main->agent->agent;
1005 if ( $FS::cust_bill::invoice_lines ) {
1007 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1009 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1014 #format address (variable for the template)
1016 @address = ( '', '', '', '', '', '' );
1017 package FS::cust_bill; #!
1018 $FS::cust_bill::_template::address[$l++] =
1019 $cust_main->payname.
1020 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1021 ? " (P.O. #". $cust_main->payinfo. ")"
1025 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1026 if $cust_main->company;
1027 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1028 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1029 if $cust_main->address2;
1030 $FS::cust_bill::_template::address[$l++] =
1031 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1032 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1033 unless $cust_main->country eq 'US';
1035 # #overdue? (variable for the template)
1036 # $FS::cust_bill::_template::overdue = (
1038 # && $today > $self->_date
1039 ## && $self->printed > 1
1040 # && $self->printed > 0
1043 #and subroutine for the template
1044 sub FS::cust_bill::_template::invoice_lines {
1045 my $lines = shift || scalar(@buf);
1047 scalar(@buf) ? shift @buf : [ '', '' ];
1053 $FS::cust_bill::_template::page = 1;
1057 push @collect, split("\n",
1058 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1060 $FS::cust_bill::_template::page++;
1063 map "$_\n", @collect;
1067 =item print_latex [ TIME [ , TEMPLATE ] ]
1069 Internal method - returns a filename of a filled-in LaTeX template for this
1070 invoice (Note: add ".tex" to get the actual filename).
1072 See print_ps and print_pdf for methods that return PostScript and PDF output.
1074 TIME an optional value used to control the printing of overdue messages. The
1075 default is now. It isn't the date of the invoice; that's the `_date' field.
1076 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1077 L<Time::Local> and L<Date::Parse> for conversion functions.
1081 #still some false laziness w/print_text
1084 my( $self, $today, $template ) = @_;
1087 my $cust_main = $self->cust_main;
1088 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1089 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1091 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1092 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1093 #my $balance_due = $self->owed + $pr_total - $cr_total;
1094 my $balance_due = $self->owed + $pr_total;
1096 #create the template
1097 $template ||= $self->_agent_template;
1098 my $templatefile = 'invoice_latex';
1099 my $suffix = length($template) ? "_$template" : '';
1100 $templatefile .= $suffix;
1101 my @invoice_template = map "$_\n", $conf->config($templatefile)
1102 or die "cannot load config file $templatefile";
1104 my($format, $text_template);
1105 if ( grep { /^%%Detail/ } @invoice_template ) {
1106 #change this to a die when the old code is removed
1107 warn "old-style invoice template $templatefile; ".
1108 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1111 $format = 'Text::Template';
1112 $text_template = new Text::Template(
1114 SOURCE => \@invoice_template,
1115 DELIMITERS => [ '[@--', '--@]' ],
1118 $text_template->compile()
1119 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1123 if ( $conf->exists('invoice_latexreturnaddress')
1124 && length($conf->exists('invoice_latexreturnaddress'))
1127 $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1129 $returnaddress = '~';
1132 my %invoice_data = (
1133 'invnum' => $self->invnum,
1134 'date' => time2str('%b %o, %Y', $self->_date),
1135 'agent' => _latex_escape($cust_main->agent->agent),
1136 'payname' => _latex_escape($cust_main->payname),
1137 'company' => _latex_escape($cust_main->company),
1138 'address1' => _latex_escape($cust_main->address1),
1139 'address2' => _latex_escape($cust_main->address2),
1140 'city' => _latex_escape($cust_main->city),
1141 'state' => _latex_escape($cust_main->state),
1142 'zip' => _latex_escape($cust_main->zip),
1143 'country' => _latex_escape($cust_main->country),
1144 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1145 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1146 'returnaddress' => $returnaddress,
1148 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1149 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1150 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1153 my $countrydefault = $conf->config('countrydefault') || 'US';
1154 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1156 #do variable substitutions in notes
1157 $invoice_data{'notes'} =
1159 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1160 $conf->config_orbase('invoice_latexnotes', $suffix)
1163 $invoice_data{'footer'} =~ s/\n+$//;
1164 $invoice_data{'smallfooter'} =~ s/\n+$//;
1165 $invoice_data{'notes'} =~ s/\n+$//;
1167 $invoice_data{'po_line'} =
1168 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1169 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1173 if ( $format eq 'old' ) {
1176 my @total_item = ();
1177 while ( @invoice_template ) {
1178 my $line = shift @invoice_template;
1180 if ( $line =~ /^%%Detail\s*$/ ) {
1182 while ( ( my $line_item_line = shift @invoice_template )
1183 !~ /^%%EndDetail\s*$/ ) {
1184 push @line_item, $line_item_line;
1186 foreach my $line_item ( $self->_items ) {
1187 #foreach my $line_item ( $self->_items_pkg ) {
1188 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1189 $invoice_data{'description'} =
1190 _latex_escape($line_item->{'description'});
1191 if ( exists $line_item->{'ext_description'} ) {
1192 $invoice_data{'description'} .=
1193 "\\tabularnewline\n~~".
1194 join( "\\tabularnewline\n~~",
1195 map _latex_escape($_), @{$line_item->{'ext_description'}}
1198 $invoice_data{'amount'} = $line_item->{'amount'};
1199 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1201 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1204 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1206 while ( ( my $total_item_line = shift @invoice_template )
1207 !~ /^%%EndTotalDetails\s*$/ ) {
1208 push @total_item, $total_item_line;
1211 my @total_fill = ();
1214 foreach my $tax ( $self->_items_tax ) {
1215 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1216 $taxtotal += $tax->{'amount'};
1217 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1219 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1224 $invoice_data{'total_item'} = 'Sub-total';
1225 $invoice_data{'total_amount'} =
1226 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1227 unshift @total_fill,
1228 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1232 $invoice_data{'total_item'} = '\textbf{Total}';
1233 $invoice_data{'total_amount'} =
1234 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1236 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1239 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1242 foreach my $credit ( $self->_items_credits ) {
1243 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1245 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1247 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1252 foreach my $payment ( $self->_items_payments ) {
1253 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1255 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1257 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1261 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1262 $invoice_data{'total_amount'} =
1263 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1265 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1268 push @filled_in, @total_fill;
1271 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1272 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1273 push @filled_in, $line;
1284 } elsif ( $format eq 'Text::Template' ) {
1286 my @detail_items = ();
1287 my @total_items = ();
1289 $invoice_data{'detail_items'} = \@detail_items;
1290 $invoice_data{'total_items'} = \@total_items;
1292 foreach my $line_item ( $self->_items ) {
1294 ext_description => [],
1296 $detail->{'ref'} = $line_item->{'pkgnum'};
1297 $detail->{'quantity'} = 1;
1298 $detail->{'description'} = _latex_escape($line_item->{'description'});
1299 if ( exists $line_item->{'ext_description'} ) {
1300 @{$detail->{'ext_description'}} = map {
1302 } @{$line_item->{'ext_description'}};
1304 $detail->{'amount'} = $line_item->{'amount'};
1305 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1307 push @detail_items, $detail;
1312 foreach my $tax ( $self->_items_tax ) {
1314 $total->{'total_item'} = _latex_escape($tax->{'description'});
1315 $taxtotal += $tax->{'amount'};
1316 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1317 push @total_items, $total;
1322 $total->{'total_item'} = 'Sub-total';
1323 $total->{'total_amount'} =
1324 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1325 unshift @total_items, $total;
1330 $total->{'total_item'} = '\textbf{Total}';
1331 $total->{'total_amount'} =
1332 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1333 push @total_items, $total;
1336 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1339 foreach my $credit ( $self->_items_credits ) {
1341 $total->{'total_item'} = _latex_escape($credit->{'description'});
1343 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1344 push @total_items, $total;
1348 foreach my $payment ( $self->_items_payments ) {
1350 $total->{'total_item'} = _latex_escape($payment->{'description'});
1352 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1353 push @total_items, $total;
1358 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1359 $total->{'total_amount'} =
1360 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1361 push @total_items, $total;
1365 die "guru meditation #54";
1368 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1369 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1373 ) or die "can't open temp file: $!\n";
1374 if ( $format eq 'old' ) {
1375 print $fh join('', @filled_in );
1376 } elsif ( $format eq 'Text::Template' ) {
1377 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1379 die "guru meditation #32";
1383 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1388 =item print_ps [ TIME [ , TEMPLATE ] ]
1390 Returns an postscript invoice, as a scalar.
1392 TIME an optional value used to control the printing of overdue messages. The
1393 default is now. It isn't the date of the invoice; that's the `_date' field.
1394 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1395 L<Time::Local> and L<Date::Parse> for conversion functions.
1402 my $file = $self->print_latex(@_);
1404 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1407 my $sfile = shell_quote $file;
1409 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1410 or die "pslatex $file.tex failed; see $file.log for details?\n";
1411 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1412 or die "pslatex $file.tex failed; see $file.log for details?\n";
1414 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1415 or die "dvips failed";
1417 open(POSTSCRIPT, "<$file.ps")
1418 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1420 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1423 while (<POSTSCRIPT>) {
1433 =item print_pdf [ TIME [ , TEMPLATE ] ]
1435 Returns an PDF invoice, as a scalar.
1437 TIME an optional value used to control the printing of overdue messages. The
1438 default is now. It isn't the date of the invoice; that's the `_date' field.
1439 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1440 L<Time::Local> and L<Date::Parse> for conversion functions.
1447 my $file = $self->print_latex(@_);
1449 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1452 #system('pdflatex', "$file.tex");
1453 #system('pdflatex', "$file.tex");
1454 #! LaTeX Error: Unknown graphics extension: .eps.
1456 my $sfile = shell_quote $file;
1458 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1459 or die "pslatex $file.tex failed; see $file.log for details?\n";
1460 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1461 or die "pslatex $file.tex failed; see $file.log for details?\n";
1463 #system('dvipdf', "$file.dvi", "$file.pdf" );
1465 "dvips -q -t letter -f $sfile.dvi ".
1466 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1469 or die "dvips | gs failed: $!";
1471 open(PDF, "<$file.pdf")
1472 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1474 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1487 =item print_html [ TIME [ , TEMPLATE ] ]
1489 Returns an HTML invoice, as a scalar.
1491 TIME an optional value used to control the printing of overdue messages. The
1492 default is now. It isn't the date of the invoice; that's the `_date' field.
1493 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1494 L<Time::Local> and L<Date::Parse> for conversion functions.
1501 # my $file = $self->print_latex(@_);
1503 # my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1506 # my $sfile = shell_quote $file;
1508 # system("htlatex $sfile.tex") == 0
1509 # or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1510 # #system("ltoh $sfile.tex") == 0
1511 # # or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1513 # open(HTML, "<$file.html")
1514 # or die "can't open $file.html: $! (error in LaTeX template?)\n";
1516 # #unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
1521 # s/<link\s+rel="stylesheet"\s+type="text\/css"\s+href="invoice\.(\d+)\.(\w+)\.css">/<link rel="stylesheet" type="text\/css" href="cust_bill.html?$1.$2.css">/;
1532 ##inefficient proof-of-concept for now
1533 #sub print_html_css {
1536 # my $file = $self->print_latex(@_);
1538 # my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1541 # my $sfile = shell_quote $file;
1543 # system("htlatex $sfile.tex") == 0
1544 # or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1545 # #system("ltoh $sfile.tex") == 0
1546 # # or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1548 # open(CSS, "<$file.css")
1549 # or die "can't open $file.html: $! (error in LaTeX template?)\n";
1551 # unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
1565 my( $self, $today, $template ) = @_;
1568 my $cust_main = $self->cust_main;
1569 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1570 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1572 $template ||= $self->_agent_template;
1573 my $templatefile = 'invoice_html';
1574 my $suffix = length($template) ? "_$template" : '';
1575 $templatefile .= $suffix;
1576 my @html_template = map "$_\n", $conf->config($templatefile)
1577 or die "cannot load config file $templatefile";
1579 my $html_template = new Text::Template(
1581 SOURCE => \@html_template,
1582 DELIMITERS => [ '<%=', '%>' ],
1585 $html_template->compile()
1586 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1588 my $returnaddress = $conf->exists('invoice_htmlreturnaddress')
1589 ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1590 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; }
1591 $conf->config('invoice_latexreturnaddress')
1593 warn $conf->config('invoice_latexreturnaddress');
1594 warn $returnaddress;
1596 my %invoice_data = (
1597 'invnum' => $self->invnum,
1598 'date' => time2str('%b %o, %Y', $self->_date),
1599 'agent' => encode_entities($cust_main->agent->agent),
1600 'payname' => encode_entities($cust_main->payname),
1601 'company' => encode_entities($cust_main->company),
1602 'address1' => encode_entities($cust_main->address1),
1603 'address2' => encode_entities($cust_main->address2),
1604 'city' => encode_entities($cust_main->city),
1605 'state' => encode_entities($cust_main->state),
1606 'zip' => encode_entities($cust_main->zip),
1607 'country' => encode_entities($cust_main->country),
1608 # 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1609 # 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1610 'returnaddress' => $returnaddress,
1611 'terms' => $conf->config('invoice_default_terms')
1612 || 'Payable upon receipt',
1613 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1614 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1617 my $countrydefault = $conf->config('countrydefault') || 'US';
1618 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1620 # #do variable substitutions in notes
1621 # $invoice_data{'notes'} =
1623 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1624 # $conf->config_orbase('invoice_latexnotes', $suffix)
1627 # $invoice_data{'footer'} =~ s/\n+$//;
1628 # $invoice_data{'smallfooter'} =~ s/\n+$//;
1629 # $invoice_data{'notes'} =~ s/\n+$//;
1631 $invoice_data{'po_line'} =
1632 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1633 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1636 my $money_char = $conf->config('money_char') || '$';
1638 foreach my $line_item ( $self->_items ) {
1640 ext_description => [],
1642 $detail->{'ref'} = $line_item->{'pkgnum'};
1643 $detail->{'description'} = encode_entities($line_item->{'description'});
1644 if ( exists $line_item->{'ext_description'} ) {
1645 @{$detail->{'ext_description'}} = map {
1646 encode_entities($_);
1647 } @{$line_item->{'ext_description'}};
1649 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1650 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1652 push @{$invoice_data{'detail_items'}}, $detail;
1657 foreach my $tax ( $self->_items_tax ) {
1659 $total->{'total_item'} = encode_entities($tax->{'description'});
1660 $taxtotal += $tax->{'amount'};
1661 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1662 push @{$invoice_data{'total_items'}}, $total;
1667 $total->{'total_item'} = 'Sub-total';
1668 $total->{'total_amount'} =
1669 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1670 unshift @{$invoice_data{'total_items'}}, $total;
1673 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1676 $total->{'total_item'} = '<b>Total</b>';
1677 $total->{'total_amount'} =
1678 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1679 push @{$invoice_data{'total_items'}}, $total;
1682 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1685 foreach my $credit ( $self->_items_credits ) {
1687 $total->{'total_item'} = encode_entities($credit->{'description'});
1689 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1690 push @{$invoice_data{'total_items'}}, $total;
1694 foreach my $payment ( $self->_items_payments ) {
1696 $total->{'total_item'} = encode_entities($payment->{'description'});
1698 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1699 push @{$invoice_data{'total_items'}}, $total;
1704 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1705 $total->{'total_amount'} =
1706 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1707 push @{$invoice_data{'total_items'}}, $total;
1710 $html_template->fill_in( HASH => \%invoice_data);
1713 # quick subroutine for print_latex
1715 # There are ten characters that LaTeX treats as special characters, which
1716 # means that they do not simply typeset themselves:
1717 # # $ % & ~ _ ^ \ { }
1719 # TeX ignores blanks following an escaped character; if you want a blank (as
1720 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1724 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1725 $value =~ s/([<>])/\$$1\$/g;
1729 #utility methods for print_*
1731 sub balance_due_msg {
1733 my $msg = 'Balance Due';
1734 return $msg unless $conf->exists('invoice_default_terms');
1735 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1736 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1737 } elsif ( $conf->config('invoice_default_terms') ) {
1738 $msg .= ' - '. $conf->config('invoice_default_terms');
1745 my @display = scalar(@_)
1747 : qw( _items_previous _items_pkg );
1748 #: qw( _items_pkg );
1749 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1751 foreach my $display ( @display ) {
1752 push @b, $self->$display(@_);
1757 sub _items_previous {
1759 my $cust_main = $self->cust_main;
1760 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1762 foreach ( @pr_cust_bill ) {
1764 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1765 ' ('. time2str('%x',$_->_date). ')',
1766 #'pkgpart' => 'N/A',
1768 'amount' => sprintf("%.2f", $_->owed),
1774 # 'description' => 'Previous Balance',
1775 # #'pkgpart' => 'N/A',
1776 # 'pkgnum' => 'N/A',
1777 # 'amount' => sprintf("%10.2f", $pr_total ),
1778 # 'ext_description' => [ map {
1779 # "Invoice ". $_->invnum.
1780 # " (". time2str("%x",$_->_date). ") ".
1781 # sprintf("%10.2f", $_->owed)
1782 # } @pr_cust_bill ],
1789 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1790 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1795 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1796 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1799 sub _items_cust_bill_pkg {
1801 my $cust_bill_pkg = shift;
1804 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1806 if ( $cust_bill_pkg->pkgnum ) {
1808 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1809 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1810 my $pkg = $part_pkg->pkg;
1812 if ( $cust_bill_pkg->setup != 0 ) {
1813 my $description = $pkg;
1814 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1815 my @d = $cust_pkg->h_labels_short($self->_date);
1816 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1818 description => $description,
1819 #pkgpart => $part_pkg->pkgpart,
1820 pkgnum => $cust_pkg->pkgnum,
1821 amount => sprintf("%.2f", $cust_bill_pkg->setup),
1822 ext_description => \@d,
1826 if ( $cust_bill_pkg->recur != 0 ) {
1828 description => "$pkg (" .
1829 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1830 time2str('%x', $cust_bill_pkg->edate). ')',
1831 #pkgpart => $part_pkg->pkgpart,
1832 pkgnum => $cust_pkg->pkgnum,
1833 amount => sprintf("%.2f", $cust_bill_pkg->recur),
1834 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1835 $cust_bill_pkg->sdate),
1836 $cust_bill_pkg->details,
1841 } else { #pkgnum tax or one-shot line item (??)
1843 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1844 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1846 if ( $cust_bill_pkg->setup != 0 ) {
1848 'description' => $itemdesc,
1849 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
1852 if ( $cust_bill_pkg->recur != 0 ) {
1854 'description' => "$itemdesc (".
1855 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1856 time2str("%x", $cust_bill_pkg->edate). ')',
1857 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
1869 sub _items_credits {
1874 foreach ( $self->cust_credited ) {
1876 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1878 my $reason = $_->cust_credit->reason;
1879 #my $reason = substr($_->cust_credit->reason,0,32);
1880 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1881 $reason = " ($reason) " if $reason;
1883 #'description' => 'Credit ref\#'. $_->crednum.
1884 # " (". time2str("%x",$_->cust_credit->_date) .")".
1886 'description' => 'Credit applied '.
1887 time2str("%x",$_->cust_credit->_date). $reason,
1888 'amount' => sprintf("%.2f",$_->amount),
1891 #foreach ( @cr_cust_credit ) {
1893 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1894 # $money_char. sprintf("%10.2f",$_->credited)
1902 sub _items_payments {
1906 #get & print payments
1907 foreach ( $self->cust_bill_pay ) {
1909 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1912 'description' => "Payment received ".
1913 time2str("%x",$_->cust_pay->_date ),
1914 'amount' => sprintf("%.2f", $_->amount )
1928 print_text formatting (and some logic :/) is in source, but needs to be
1929 slurped in from a file. Also number of lines ($=).
1933 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1934 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base