4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
9 use String::ShellQuote;
10 use FS::UID qw( datasrc );
11 use FS::Record qw( qsearch qsearchs );
12 use FS::Misc qw( send_email );
14 use FS::cust_bill_pkg;
18 use FS::cust_credit_bill;
19 use FS::cust_pay_batch;
20 use FS::cust_bill_event;
22 @ISA = qw( FS::Record );
24 #ask FS::UID to run this stuff for us later
25 FS::UID->install_callback( sub {
27 $money_char = $conf->config('money_char') || '$';
32 FS::cust_bill - Object methods for cust_bill records
38 $record = new FS::cust_bill \%hash;
39 $record = new FS::cust_bill { 'column' => 'value' };
41 $error = $record->insert;
43 $error = $new_record->replace($old_record);
45 $error = $record->delete;
47 $error = $record->check;
49 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
51 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
53 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
55 @cust_pay_objects = $cust_bill->cust_pay;
57 $tax_amount = $record->tax;
59 @lines = $cust_bill->print_text;
60 @lines = $cust_bill->print_text $time;
64 An FS::cust_bill object represents an invoice; a declaration that a customer
65 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
66 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
67 following fields are currently supported:
71 =item invnum - primary key (assigned automatically for new invoices)
73 =item custnum - customer (see L<FS::cust_main>)
75 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
76 L<Time::Local> and L<Date::Parse> for conversion functions.
78 =item charged - amount of this invoice
80 =item printed - deprecated
82 =item closed - books closed flag, empty or `Y'
92 Creates a new invoice. To add the invoice to the database, see L<"insert">.
93 Invoices are normally created by calling the bill method of a customer object
94 (see L<FS::cust_main>).
98 sub table { 'cust_bill'; }
102 Adds this invoice to the database ("Posts" the invoice). If there is an error,
103 returns the error, otherwise returns false.
107 Currently unimplemented. I don't remove invoices because there would then be
108 no record you ever posted this invoice (which is bad, no?)
114 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
115 $self->SUPER::delete(@_);
118 =item replace OLD_RECORD
120 Replaces the OLD_RECORD with this one in the database. If there is an error,
121 returns the error, otherwise returns false.
123 Only printed may be changed. printed is normally updated by calling the
124 collect method of a customer object (see L<FS::cust_main>).
129 my( $new, $old ) = ( shift, shift );
130 return "Can't change custnum!" unless $old->custnum == $new->custnum;
131 #return "Can't change _date!" unless $old->_date eq $new->_date;
132 return "Can't change _date!" unless $old->_date == $new->_date;
133 return "Can't change charged!" unless $old->charged == $new->charged;
135 $new->SUPER::replace($old);
140 Checks all fields to make sure this is a valid invoice. If there is an error,
141 returns the error, otherwise returns false. Called by the insert and replace
150 $self->ut_numbern('invnum')
151 || $self->ut_number('custnum')
152 || $self->ut_numbern('_date')
153 || $self->ut_money('charged')
154 || $self->ut_numbern('printed')
155 || $self->ut_enum('closed', [ '', 'Y' ])
157 return $error if $error;
159 return "Unknown customer"
160 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
162 $self->_date(time) unless $self->_date;
164 $self->printed(0) if $self->printed eq '';
171 Returns a list consisting of the total previous balance for this customer,
172 followed by the previous outstanding invoices (as FS::cust_bill objects also).
179 my @cust_bill = sort { $a->_date <=> $b->_date }
180 grep { $_->owed != 0 && $_->_date < $self->_date }
181 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
183 foreach ( @cust_bill ) { $total += $_->owed; }
189 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
195 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
198 =item cust_bill_event
200 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
205 sub cust_bill_event {
207 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
213 Returns the customer (see L<FS::cust_main>) for this invoice.
219 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
224 Depreciated. See the cust_credited method.
226 #Returns a list consisting of the total previous credited (see
227 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
228 #outstanding credits (FS::cust_credit objects).
234 croak "FS::cust_bill->cust_credit depreciated; see ".
235 "FS::cust_bill->cust_credit_bill";
238 #my @cust_credit = sort { $a->_date <=> $b->_date }
239 # grep { $_->credited != 0 && $_->_date < $self->_date }
240 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
242 #foreach (@cust_credit) { $total += $_->credited; }
243 #$total, @cust_credit;
248 Depreciated. See the cust_bill_pay method.
250 #Returns all payments (see L<FS::cust_pay>) for this invoice.
256 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
258 #sort { $a->_date <=> $b->_date }
259 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
265 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
271 sort { $a->_date <=> $b->_date }
272 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
277 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
283 sort { $a->_date <=> $b->_date }
284 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
290 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
297 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
299 foreach (@taxlines) { $total += $_->setup; }
305 Returns the amount owed (still outstanding) on this invoice, which is charged
306 minus all payment applications (see L<FS::cust_bill_pay>) and credit
307 applications (see L<FS::cust_credit_bill>).
313 my $balance = $self->charged;
314 $balance -= $_->amount foreach ( $self->cust_bill_pay );
315 $balance -= $_->amount foreach ( $self->cust_credited );
316 $balance = sprintf( "%.2f", $balance);
317 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
321 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
323 Sends this invoice to the destinations configured for this customer: send
324 emails or print. See L<FS::cust_main_invoice>.
326 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
328 AGENTNUM, if specified, means that this invoice will only be sent for customers
329 of the specified agent.
331 INVOICE_FROM, if specified, overrides the default email invoice From: address.
337 my $template = scalar(@_) ? shift : '';
338 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
342 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
344 my @print_text = $self->print_text('', $template);
345 my @invoicing_list = $self->cust_main->invoicing_list;
347 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
349 #better to notify this person than silence
350 @invoicing_list = ($invoice_from) unless @invoicing_list;
352 my $error = send_email(
353 'from' => $invoice_from,
354 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
355 'subject' => 'Invoice',
356 'body' => \@print_text,
358 die "can't email invoice: $error\n" if $error;
362 if ( $conf->config('invoice_latex') ) {
363 @print_text = $self->print_ps('', $template);
366 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
367 my $lpr = $conf->config('lpr');
369 or die "Can't open pipe to $lpr: $!\n";
370 print LPR @print_text;
372 or die $! ? "Error closing $lpr: $!\n"
373 : "Exit status $? from $lpr\n";
380 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
382 Like B<send>, but only sends the invoice if it is the newest open invoice for
392 grep { $_->owed > 0 }
393 qsearch('cust_bill', {
394 'custnum' => $self->custnum,
395 #'_date' => { op=>'>', value=>$self->_date },
396 'invnum' => { op=>'>', value=>$self->invnum },
403 =item send_csv OPTIONS
405 Sends invoice as a CSV data-file to a remote host with the specified protocol.
409 protocol - currently only "ftp"
415 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
416 and YYMMDDHHMMSS is a timestamp.
418 The fields of the CSV file is as follows:
420 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
424 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
426 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
427 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
428 fields are filled in.
430 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
431 first two fields (B<record_type> and B<invnum>) and the last five fields
432 (B<pkg> through B<edate>) are filled in.
434 =item invnum - invoice number
436 =item custnum - customer number
438 =item _date - invoice date
440 =item charged - total invoice amount
442 =item first - customer first name
444 =item last - customer first name
446 =item company - company name
448 =item address1 - address line 1
450 =item address2 - address line 1
460 =item pkg - line item description
462 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
464 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
466 =item sdate - start date for recurring fee
468 =item edate - end date for recurring fee
475 my($self, %opt) = @_;
477 #part one: create file
479 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
480 mkdir $spooldir, 0700 unless -d $spooldir;
482 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
484 open(CSV, ">$file") or die "can't open $file: $!";
486 eval "use Text::CSV_XS";
489 my $csv = Text::CSV_XS->new({'always_quote'=>1});
491 my $cust_main = $self->cust_main;
497 time2str("%x", $self->_date),
498 sprintf("%.2f", $self->charged),
499 ( map { $cust_main->getfield($_) }
500 qw( first last company address1 address2 city state zip country ) ),
502 ) or die "can't create csv";
503 print CSV $csv->string. "\n";
505 #new charges (false laziness w/print_text)
506 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
508 my($pkg, $setup, $recur, $sdate, $edate);
509 if ( $cust_bill_pkg->pkgnum ) {
511 ($pkg, $setup, $recur, $sdate, $edate) = (
512 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
513 ( $cust_bill_pkg->setup != 0
514 ? sprintf("%.2f", $cust_bill_pkg->setup )
516 ( $cust_bill_pkg->recur != 0
517 ? sprintf("%.2f", $cust_bill_pkg->recur )
519 time2str("%x", $cust_bill_pkg->sdate),
520 time2str("%x", $cust_bill_pkg->edate),
524 next unless $cust_bill_pkg->setup != 0;
525 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
526 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
528 ($pkg, $setup, $recur, $sdate, $edate) =
529 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
535 ( map { '' } (1..11) ),
536 ($pkg, $setup, $recur, $sdate, $edate)
537 ) or die "can't create csv";
538 print CSV $csv->string. "\n";
542 close CSV or die "can't close CSV: $!";
547 if ( $opt{protocol} eq 'ftp' ) {
548 eval "use Net::FTP;";
550 $net = Net::FTP->new($opt{server}) or die @$;
552 die "unknown protocol: $opt{protocol}";
555 $net->login( $opt{username}, $opt{password} )
556 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
558 $net->binary or die "can't set binary mode";
560 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
562 $net->put($file) or die "can't put $file: $!";
572 Pays this invoice with a compliemntary payment. If there is an error,
573 returns the error, otherwise returns false.
579 my $cust_pay = new FS::cust_pay ( {
580 'invnum' => $self->invnum,
581 'paid' => $self->owed,
584 'payinfo' => $self->cust_main->payinfo,
592 Attempts to pay this invoice with a credit card payment via a
593 Business::OnlinePayment realtime gateway. See
594 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
595 for supported processors.
601 $self->realtime_bop( 'CC', @_ );
606 Attempts to pay this invoice with an electronic check (ACH) payment via a
607 Business::OnlinePayment realtime gateway. See
608 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
609 for supported processors.
615 $self->realtime_bop( 'ECHECK', @_ );
620 Attempts to pay this invoice with phone bill (LEC) payment via a
621 Business::OnlinePayment realtime gateway. See
622 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
623 for supported processors.
629 $self->realtime_bop( 'LEC', @_ );
633 my( $self, $method ) = @_;
635 my $cust_main = $self->cust_main;
636 my $balance = $cust_main->balance;
637 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
638 $amount = sprintf("%.2f", $amount);
639 return "not run (balance $balance)" unless $amount > 0;
641 my $description = 'Internet Services';
642 if ( $conf->exists('business-onlinepayment-description') ) {
643 my $dtempl = $conf->config('business-onlinepayment-description');
645 my $agent_obj = $cust_main->agent
646 or die "can't retreive agent for $cust_main (agentnum ".
647 $cust_main->agentnum. ")";
648 my $agent = $agent_obj->agent;
649 my $pkgs = join(', ',
650 map { $_->cust_pkg->part_pkg->pkg }
651 grep { $_->pkgnum } $self->cust_bill_pkg
653 $description = eval qq("$dtempl");
656 $cust_main->realtime_bop($method, $amount,
657 'description' => $description,
658 'invnum' => $self->invnum,
665 Adds a payment for this invoice to the pending credit card batch (see
666 L<FS::cust_pay_batch>).
672 my $cust_main = $self->cust_main;
674 my $cust_pay_batch = new FS::cust_pay_batch ( {
675 'invnum' => $self->getfield('invnum'),
676 'custnum' => $cust_main->getfield('custnum'),
677 'last' => $cust_main->getfield('last'),
678 'first' => $cust_main->getfield('first'),
679 'address1' => $cust_main->getfield('address1'),
680 'address2' => $cust_main->getfield('address2'),
681 'city' => $cust_main->getfield('city'),
682 'state' => $cust_main->getfield('state'),
683 'zip' => $cust_main->getfield('zip'),
684 'country' => $cust_main->getfield('country'),
685 'cardnum' => $cust_main->getfield('payinfo'),
686 'exp' => $cust_main->getfield('paydate'),
687 'payname' => $cust_main->getfield('payname'),
688 'amount' => $self->owed,
690 my $error = $cust_pay_batch->insert;
691 die $error if $error;
696 sub _agent_template {
698 $self->_agent_plandata('agent_templatename');
701 sub _agent_invoice_from {
703 $self->_agent_plandata('agent_invoice_from');
706 sub _agent_plandata {
707 my( $self, $option ) = @_;
709 my $part_bill_event = qsearchs( 'part_bill_event',
711 'payby' => $self->cust_main->payby,
712 'plan' => 'send_agent',
713 'plandata' => { 'op' => '~',
714 'value' => "(^|\n)agentnum ".
715 $self->cust_main->agentnum.
720 'ORDER BY seconds LIMIT 1'
723 return '' unless $part_bill_event;
725 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
728 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
729 " plandata for $option";
735 =item print_text [ TIME [ , TEMPLATE ] ]
737 Returns an text invoice, as a list of lines.
739 TIME an optional value used to control the printing of overdue messages. The
740 default is now. It isn't the date of the invoice; that's the `_date' field.
741 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
742 L<Time::Local> and L<Date::Parse> for conversion functions.
746 #still some false laziness w/print_text
749 my( $self, $today, $template ) = @_;
752 # my $invnum = $self->invnum;
753 my $cust_main = $self->cust_main;
754 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
755 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
757 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
758 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
759 #my $balance_due = $self->owed + $pr_total - $cr_total;
760 my $balance_due = $self->owed + $pr_total;
763 #my($description,$amount);
767 foreach ( @pr_cust_bill ) {
769 "Previous Balance, Invoice #". $_->invnum.
770 " (". time2str("%x",$_->_date). ")",
771 $money_char. sprintf("%10.2f",$_->owed)
775 push @buf,['','-----------'];
776 push @buf,[ 'Total Previous Balance',
777 $money_char. sprintf("%10.2f",$pr_total ) ];
782 foreach my $cust_bill_pkg (
783 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
784 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
787 if ( $cust_bill_pkg->pkgnum ) {
789 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
790 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
791 my $pkg = $part_pkg->pkg;
793 if ( $cust_bill_pkg->setup != 0 ) {
794 my $description = $pkg;
795 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
796 push @buf, [ $description,
797 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
799 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
802 if ( $cust_bill_pkg->recur != 0 ) {
804 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
805 time2str("%x", $cust_bill_pkg->edate) . ")",
806 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
809 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
812 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
814 } else { #pkgnum tax or one-shot line item
815 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
816 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
818 if ( $cust_bill_pkg->setup != 0 ) {
819 push @buf, [ $itemdesc,
820 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
822 if ( $cust_bill_pkg->recur != 0 ) {
823 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
824 . time2str("%x", $cust_bill_pkg->edate). ")",
825 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
831 push @buf,['','-----------'];
832 push @buf,['Total New Charges',
833 $money_char. sprintf("%10.2f",$self->charged) ];
836 push @buf,['','-----------'];
837 push @buf,['Total Charges',
838 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
842 foreach ( $self->cust_credited ) {
844 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
846 my $reason = substr($_->cust_credit->reason,0,32);
847 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
848 $reason = " ($reason) " if $reason;
850 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
852 $money_char. sprintf("%10.2f",$_->amount)
855 #foreach ( @cr_cust_credit ) {
857 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
858 # $money_char. sprintf("%10.2f",$_->credited)
862 #get & print payments
863 foreach ( $self->cust_bill_pay ) {
865 #something more elaborate if $_->amount ne ->cust_pay->paid ?
868 "Payment received ". time2str("%x",$_->cust_pay->_date ),
869 $money_char. sprintf("%10.2f",$_->amount )
874 my $balance_due_msg = $self->balance_due_msg;
876 push @buf,['','-----------'];
877 push @buf,[$balance_due_msg, $money_char.
878 sprintf("%10.2f", $balance_due ) ];
881 $template ||= $self->_agent_template;
882 my $templatefile = 'invoice_template';
883 $templatefile .= "_$template" if length($template);
884 my @invoice_template = $conf->config($templatefile)
885 or die "cannot load config file $templatefile";
888 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
889 /invoice_lines\((\d*)\)/;
890 $invoice_lines += $1 || scalar(@buf);
893 die "no invoice_lines() functions in template?" unless $wasfunc;
894 my $invoice_template = new Text::Template (
896 SOURCE => [ map "$_\n", @invoice_template ],
897 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
898 $invoice_template->compile()
899 or die "can't compile template: $Text::Template::ERROR";
901 #setup template variables
902 package FS::cust_bill::_template; #!
903 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
905 $invnum = $self->invnum;
906 $date = $self->_date;
908 $agent = $self->cust_main->agent->agent;
910 if ( $FS::cust_bill::invoice_lines ) {
912 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
914 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
919 #format address (variable for the template)
921 @address = ( '', '', '', '', '', '' );
922 package FS::cust_bill; #!
923 $FS::cust_bill::_template::address[$l++] =
925 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
926 ? " (P.O. #". $cust_main->payinfo. ")"
930 $FS::cust_bill::_template::address[$l++] = $cust_main->company
931 if $cust_main->company;
932 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
933 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
934 if $cust_main->address2;
935 $FS::cust_bill::_template::address[$l++] =
936 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
937 $FS::cust_bill::_template::address[$l++] = $cust_main->country
938 unless $cust_main->country eq 'US';
940 # #overdue? (variable for the template)
941 # $FS::cust_bill::_template::overdue = (
943 # && $today > $self->_date
944 ## && $self->printed > 1
945 # && $self->printed > 0
948 #and subroutine for the template
949 sub FS::cust_bill::_template::invoice_lines {
950 my $lines = shift || scalar(@buf);
952 scalar(@buf) ? shift @buf : [ '', '' ];
958 $FS::cust_bill::_template::page = 1;
962 push @collect, split("\n",
963 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
965 $FS::cust_bill::_template::page++;
968 map "$_\n", @collect;
972 =item print_latex [ TIME [ , TEMPLATE ] ]
974 Internal method - returns a filename of a filled-in LaTeX template for this
975 invoice (Note: add ".tex" to get the actual filename).
977 See print_ps and print_pdf for methods that return PostScript and PDF output.
979 TIME an optional value used to control the printing of overdue messages. The
980 default is now. It isn't the date of the invoice; that's the `_date' field.
981 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
982 L<Time::Local> and L<Date::Parse> for conversion functions.
986 #still some false laziness w/print_text
989 my( $self, $today, $template ) = @_;
992 # my $invnum = $self->invnum;
993 my $cust_main = $self->cust_main;
994 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
995 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
997 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
998 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
999 #my $balance_due = $self->owed + $pr_total - $cr_total;
1000 my $balance_due = $self->owed + $pr_total;
1003 #my($description,$amount);
1006 #create the template
1007 $template ||= $self->_agent_template;
1008 my $templatefile = 'invoice_latex';
1009 my $suffix = length($template) ? "_$template" : '';
1010 $templatefile .= $suffix;
1011 my @invoice_template = $conf->config($templatefile)
1012 or die "cannot load config file $templatefile";
1014 my %invoice_data = (
1015 'invnum' => $self->invnum,
1016 'date' => time2str('%b %o, %Y', $self->_date),
1017 'agent' => _latex_escape($cust_main->agent->agent),
1018 'payname' => _latex_escape($cust_main->payname),
1019 'company' => _latex_escape($cust_main->company),
1020 'address1' => _latex_escape($cust_main->address1),
1021 'address2' => _latex_escape($cust_main->address2),
1022 'city' => _latex_escape($cust_main->city),
1023 'state' => _latex_escape($cust_main->state),
1024 'zip' => _latex_escape($cust_main->zip),
1025 'country' => _latex_escape($cust_main->country),
1026 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1027 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1029 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1030 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1033 my $countrydefault = $conf->config('countrydefault') || 'US';
1034 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1036 #do variable substitutions in notes
1037 $invoice_data{'notes'} =
1039 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1040 $conf->config_orbase('invoice_latexnotes', $suffix)
1043 $invoice_data{'footer'} =~ s/\n+$//;
1044 $invoice_data{'smallfooter'} =~ s/\n+$//;
1045 $invoice_data{'notes'} =~ s/\n+$//;
1047 $invoice_data{'po_line'} =
1048 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1049 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1053 my @total_item = ();
1055 while ( @invoice_template ) {
1056 my $line = shift @invoice_template;
1058 if ( $line =~ /^%%Detail\s*$/ ) {
1060 while ( ( my $line_item_line = shift @invoice_template )
1061 !~ /^%%EndDetail\s*$/ ) {
1062 push @line_item, $line_item_line;
1064 foreach my $line_item ( $self->_items ) {
1065 #foreach my $line_item ( $self->_items_pkg ) {
1066 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1067 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1068 if ( exists $line_item->{'ext_description'} ) {
1069 $invoice_data{'description'} .=
1070 "\\tabularnewline\n~~".
1071 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1073 $invoice_data{'amount'} = $line_item->{'amount'};
1074 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1076 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1079 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1081 while ( ( my $total_item_line = shift @invoice_template )
1082 !~ /^%%EndTotalDetails\s*$/ ) {
1083 push @total_item, $total_item_line;
1086 my @total_fill = ();
1089 foreach my $tax ( $self->_items_tax ) {
1090 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1091 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1093 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1098 $invoice_data{'total_item'} = 'Sub-total';
1099 $invoice_data{'total_amount'} =
1100 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1101 unshift @total_fill,
1102 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1106 $invoice_data{'total_item'} = '\textbf{Total}';
1107 $invoice_data{'total_amount'} =
1108 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1110 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1113 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1116 foreach my $credit ( $self->_items_credits ) {
1117 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1119 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1121 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1126 foreach my $payment ( $self->_items_payments ) {
1127 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1129 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1131 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1135 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1136 $invoice_data{'total_amount'} =
1137 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1139 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1142 push @filled_in, @total_fill;
1145 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1146 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1147 push @filled_in, $line;
1158 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1159 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1163 ) or die "can't open temp file: $!\n";
1164 print $fh join("\n", @filled_in ), "\n";
1167 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1172 =item print_ps [ TIME [ , TEMPLATE ] ]
1174 Returns an postscript invoice, as a scalar.
1176 TIME an optional value used to control the printing of overdue messages. The
1177 default is now. It isn't the date of the invoice; that's the `_date' field.
1178 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1179 L<Time::Local> and L<Date::Parse> for conversion functions.
1186 my $file = $self->print_latex(@_);
1188 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1191 my $sfile = shell_quote $file;
1193 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1194 or die "pslatex $file.tex failed: $!";
1195 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1196 or die "pslatex $file.tex failed: $!";
1198 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1199 or die "dvips failed: $!";
1201 open(POSTSCRIPT, "<$file.ps")
1202 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1204 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1207 while (<POSTSCRIPT>) {
1217 =item print_pdf [ TIME [ , TEMPLATE ] ]
1219 Returns an PDF invoice, as a scalar.
1221 TIME an optional value used to control the printing of overdue messages. The
1222 default is now. It isn't the date of the invoice; that's the `_date' field.
1223 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1224 L<Time::Local> and L<Date::Parse> for conversion functions.
1231 my $file = $self->print_latex(@_);
1233 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1236 #system('pdflatex', "$file.tex");
1237 #system('pdflatex', "$file.tex");
1238 #! LaTeX Error: Unknown graphics extension: .eps.
1240 my $sfile = shell_quote $file;
1242 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1243 or die "pslatex $file.tex failed: $!";
1244 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1245 or die "pslatex $file.tex failed: $!";
1247 #system('dvipdf', "$file.dvi", "$file.pdf" );
1249 "dvips -q -t letter -f $sfile.dvi ".
1250 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1253 or die "dvips | gs failed: $!";
1255 open(PDF, "<$file.pdf")
1256 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1258 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1271 # quick subroutine for print_latex
1273 # There are ten characters that LaTeX treats as special characters, which
1274 # means that they do not simply typeset themselves:
1275 # # $ % & ~ _ ^ \ { }
1277 # TeX ignores blanks following an escaped character; if you want a blank (as
1278 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1282 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1286 #utility methods for print_*
1288 sub balance_due_msg {
1290 my $msg = 'Balance Due';
1291 return $msg unless $conf->exists('invoice_default_terms');
1292 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1293 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1294 } elsif ( $conf->config('invoice_default_terms') ) {
1295 $msg .= ' - '. $conf->config('invoice_default_terms');
1302 my @display = scalar(@_)
1304 : qw( _items_previous _items_pkg );
1305 #: qw( _items_pkg );
1306 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1308 foreach my $display ( @display ) {
1309 push @b, $self->$display(@_);
1314 sub _items_previous {
1316 my $cust_main = $self->cust_main;
1317 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1319 foreach ( @pr_cust_bill ) {
1321 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1322 ' ('. time2str('%x',$_->_date). ')',
1323 #'pkgpart' => 'N/A',
1325 'amount' => sprintf("%10.2f", $_->owed),
1331 # 'description' => 'Previous Balance',
1332 # #'pkgpart' => 'N/A',
1333 # 'pkgnum' => 'N/A',
1334 # 'amount' => sprintf("%10.2f", $pr_total ),
1335 # 'ext_description' => [ map {
1336 # "Invoice ". $_->invnum.
1337 # " (". time2str("%x",$_->_date). ") ".
1338 # sprintf("%10.2f", $_->owed)
1339 # } @pr_cust_bill ],
1346 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1347 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1352 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1353 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1356 sub _items_cust_bill_pkg {
1358 my $cust_bill_pkg = shift;
1361 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1363 if ( $cust_bill_pkg->pkgnum ) {
1365 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1366 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1367 my $pkg = $part_pkg->pkg;
1370 #tie %labels, 'Tie::IxHash';
1371 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1372 my @ext_description;
1373 foreach my $label ( keys %labels ) {
1374 my @values = @{ $labels{$label} };
1375 my $num = scalar(@values);
1377 push @ext_description, "$label ($num)";
1379 push @ext_description, map { "$label: $_" } @values;
1383 if ( $cust_bill_pkg->setup != 0 ) {
1384 my $description = $pkg;
1385 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1386 my @d = @ext_description;
1387 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1389 'description' => $description,
1390 #'pkgpart' => $part_pkg->pkgpart,
1391 'pkgnum' => $cust_pkg->pkgnum,
1392 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1393 'ext_description' => \@d,
1397 if ( $cust_bill_pkg->recur != 0 ) {
1399 'description' => "$pkg (" .
1400 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1401 time2str('%x', $cust_bill_pkg->edate). ')',
1402 #'pkgpart' => $part_pkg->pkgpart,
1403 'pkgnum' => $cust_pkg->pkgnum,
1404 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1405 'ext_description' => [ @ext_description,
1406 $cust_bill_pkg->details,
1411 } else { #pkgnum tax or one-shot line item (??)
1413 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1414 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1416 if ( $cust_bill_pkg->setup != 0 ) {
1418 'description' => $itemdesc,
1419 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1422 if ( $cust_bill_pkg->recur != 0 ) {
1424 'description' => "$itemdesc (".
1425 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1426 time2str("%x", $cust_bill_pkg->edate). ')',
1427 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1439 sub _items_credits {
1444 foreach ( $self->cust_credited ) {
1446 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1448 my $reason = $_->cust_credit->reason;
1449 #my $reason = substr($_->cust_credit->reason,0,32);
1450 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1451 $reason = " ($reason) " if $reason;
1453 #'description' => 'Credit ref\#'. $_->crednum.
1454 # " (". time2str("%x",$_->cust_credit->_date) .")".
1456 'description' => 'Credit applied '.
1457 time2str("%x",$_->cust_credit->_date). $reason,
1458 'amount' => sprintf("%10.2f",$_->amount),
1461 #foreach ( @cr_cust_credit ) {
1463 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1464 # $money_char. sprintf("%10.2f",$_->credited)
1472 sub _items_payments {
1476 #get & print payments
1477 foreach ( $self->cust_bill_pay ) {
1479 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1482 'description' => "Payment received ".
1483 time2str("%x",$_->cust_pay->_date ),
1484 'amount' => sprintf("%10.2f", $_->amount )
1498 print_text formatting (and some logic :/) is in source, but needs to be
1499 slurped in from a file. Also number of lines ($=).
1503 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1504 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base