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_csv OPTIONS
382 Sends invoice as a CSV data-file to a remote host with the specified protocol.
386 protocol - currently only "ftp"
392 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
393 and YYMMDDHHMMSS is a timestamp.
395 The fields of the CSV file is as follows:
397 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
401 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
403 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
404 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
405 fields are filled in.
407 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
408 first two fields (B<record_type> and B<invnum>) and the last five fields
409 (B<pkg> through B<edate>) are filled in.
411 =item invnum - invoice number
413 =item custnum - customer number
415 =item _date - invoice date
417 =item charged - total invoice amount
419 =item first - customer first name
421 =item last - customer first name
423 =item company - company name
425 =item address1 - address line 1
427 =item address2 - address line 1
437 =item pkg - line item description
439 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
441 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
443 =item sdate - start date for recurring fee
445 =item edate - end date for recurring fee
452 my($self, %opt) = @_;
454 #part one: create file
456 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
457 mkdir $spooldir, 0700 unless -d $spooldir;
459 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
461 open(CSV, ">$file") or die "can't open $file: $!";
463 eval "use Text::CSV_XS";
466 my $csv = Text::CSV_XS->new({'always_quote'=>1});
468 my $cust_main = $self->cust_main;
474 time2str("%x", $self->_date),
475 sprintf("%.2f", $self->charged),
476 ( map { $cust_main->getfield($_) }
477 qw( first last company address1 address2 city state zip country ) ),
479 ) or die "can't create csv";
480 print CSV $csv->string. "\n";
482 #new charges (false laziness w/print_text)
483 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
485 my($pkg, $setup, $recur, $sdate, $edate);
486 if ( $cust_bill_pkg->pkgnum ) {
488 ($pkg, $setup, $recur, $sdate, $edate) = (
489 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
490 ( $cust_bill_pkg->setup != 0
491 ? sprintf("%.2f", $cust_bill_pkg->setup )
493 ( $cust_bill_pkg->recur != 0
494 ? sprintf("%.2f", $cust_bill_pkg->recur )
496 time2str("%x", $cust_bill_pkg->sdate),
497 time2str("%x", $cust_bill_pkg->edate),
501 next unless $cust_bill_pkg->setup != 0;
502 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
503 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
505 ($pkg, $setup, $recur, $sdate, $edate) =
506 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
512 ( map { '' } (1..11) ),
513 ($pkg, $setup, $recur, $sdate, $edate)
514 ) or die "can't create csv";
515 print CSV $csv->string. "\n";
519 close CSV or die "can't close CSV: $!";
524 if ( $opt{protocol} eq 'ftp' ) {
525 eval "use Net::FTP;";
527 $net = Net::FTP->new($opt{server}) or die @$;
529 die "unknown protocol: $opt{protocol}";
532 $net->login( $opt{username}, $opt{password} )
533 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
535 $net->binary or die "can't set binary mode";
537 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
539 $net->put($file) or die "can't put $file: $!";
549 Pays this invoice with a compliemntary payment. If there is an error,
550 returns the error, otherwise returns false.
556 my $cust_pay = new FS::cust_pay ( {
557 'invnum' => $self->invnum,
558 'paid' => $self->owed,
561 'payinfo' => $self->cust_main->payinfo,
569 Attempts to pay this invoice with a credit card payment via a
570 Business::OnlinePayment realtime gateway. See
571 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
572 for supported processors.
578 $self->realtime_bop( 'CC', @_ );
583 Attempts to pay this invoice with an electronic check (ACH) payment via a
584 Business::OnlinePayment realtime gateway. See
585 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
586 for supported processors.
592 $self->realtime_bop( 'ECHECK', @_ );
597 Attempts to pay this invoice with phone bill (LEC) payment via a
598 Business::OnlinePayment realtime gateway. See
599 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
600 for supported processors.
606 $self->realtime_bop( 'LEC', @_ );
610 my( $self, $method ) = @_;
612 my $cust_main = $self->cust_main;
613 my $balance = $cust_main->balance;
614 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
615 $amount = sprintf("%.2f", $amount);
616 return "not run (balance $balance)" unless $amount > 0;
618 my $description = 'Internet Services';
619 if ( $conf->exists('business-onlinepayment-description') ) {
620 my $dtempl = $conf->config('business-onlinepayment-description');
622 my $agent_obj = $cust_main->agent
623 or die "can't retreive agent for $cust_main (agentnum ".
624 $cust_main->agentnum. ")";
625 my $agent = $agent_obj->agent;
626 my $pkgs = join(', ',
627 map { $_->cust_pkg->part_pkg->pkg }
628 grep { $_->pkgnum } $self->cust_bill_pkg
630 $description = eval qq("$dtempl");
633 $cust_main->realtime_bop($method, $amount,
634 'description' => $description,
635 'invnum' => $self->invnum,
642 Adds a payment for this invoice to the pending credit card batch (see
643 L<FS::cust_pay_batch>).
649 my $cust_main = $self->cust_main;
651 my $cust_pay_batch = new FS::cust_pay_batch ( {
652 'invnum' => $self->getfield('invnum'),
653 'custnum' => $cust_main->getfield('custnum'),
654 'last' => $cust_main->getfield('last'),
655 'first' => $cust_main->getfield('first'),
656 'address1' => $cust_main->getfield('address1'),
657 'address2' => $cust_main->getfield('address2'),
658 'city' => $cust_main->getfield('city'),
659 'state' => $cust_main->getfield('state'),
660 'zip' => $cust_main->getfield('zip'),
661 'country' => $cust_main->getfield('country'),
662 'cardnum' => $cust_main->getfield('payinfo'),
663 'exp' => $cust_main->getfield('paydate'),
664 'payname' => $cust_main->getfield('payname'),
665 'amount' => $self->owed,
667 my $error = $cust_pay_batch->insert;
668 die $error if $error;
673 sub _agent_template {
675 $self->_agent_plandata('agent_templatename');
678 sub _agent_invoice_from {
680 $self->_agent_plandata('agent_invoice_from');
683 sub _agent_plandata {
684 my( $self, $option ) = @_;
686 my $part_bill_event = qsearchs( 'part_bill_event',
688 'payby' => $self->cust_main->payby,
689 'plan' => 'send_agent',
690 'plandata' => { 'op' => '~',
691 'value' => "(^|\n)agentnum ".
692 $self->cust_main->agentnum.
697 'ORDER BY seconds LIMIT 1'
700 return '' unless $part_bill_event;
702 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
705 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
706 " plandata for $option";
712 =item print_text [ TIME [ , TEMPLATE ] ]
714 Returns an text invoice, as a list of lines.
716 TIME an optional value used to control the printing of overdue messages. The
717 default is now. It isn't the date of the invoice; that's the `_date' field.
718 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
719 L<Time::Local> and L<Date::Parse> for conversion functions.
723 #still some false laziness w/print_text
726 my( $self, $today, $template ) = @_;
729 # my $invnum = $self->invnum;
730 my $cust_main = $self->cust_main;
731 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
732 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
734 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
735 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
736 #my $balance_due = $self->owed + $pr_total - $cr_total;
737 my $balance_due = $self->owed + $pr_total;
740 #my($description,$amount);
744 foreach ( @pr_cust_bill ) {
746 "Previous Balance, Invoice #". $_->invnum.
747 " (". time2str("%x",$_->_date). ")",
748 $money_char. sprintf("%10.2f",$_->owed)
752 push @buf,['','-----------'];
753 push @buf,[ 'Total Previous Balance',
754 $money_char. sprintf("%10.2f",$pr_total ) ];
759 foreach my $cust_bill_pkg (
760 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
761 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
764 if ( $cust_bill_pkg->pkgnum ) {
766 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
767 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
768 my $pkg = $part_pkg->pkg;
770 if ( $cust_bill_pkg->setup != 0 ) {
771 my $description = $pkg;
772 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
773 push @buf, [ $description,
774 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
776 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
779 if ( $cust_bill_pkg->recur != 0 ) {
781 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
782 time2str("%x", $cust_bill_pkg->edate) . ")",
783 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
786 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
789 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
791 } else { #pkgnum tax or one-shot line item
792 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
793 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
795 if ( $cust_bill_pkg->setup != 0 ) {
796 push @buf, [ $itemdesc,
797 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
799 if ( $cust_bill_pkg->recur != 0 ) {
800 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
801 . time2str("%x", $cust_bill_pkg->edate). ")",
802 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
808 push @buf,['','-----------'];
809 push @buf,['Total New Charges',
810 $money_char. sprintf("%10.2f",$self->charged) ];
813 push @buf,['','-----------'];
814 push @buf,['Total Charges',
815 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
819 foreach ( $self->cust_credited ) {
821 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
823 my $reason = substr($_->cust_credit->reason,0,32);
824 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
825 $reason = " ($reason) " if $reason;
827 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
829 $money_char. sprintf("%10.2f",$_->amount)
832 #foreach ( @cr_cust_credit ) {
834 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
835 # $money_char. sprintf("%10.2f",$_->credited)
839 #get & print payments
840 foreach ( $self->cust_bill_pay ) {
842 #something more elaborate if $_->amount ne ->cust_pay->paid ?
845 "Payment received ". time2str("%x",$_->cust_pay->_date ),
846 $money_char. sprintf("%10.2f",$_->amount )
851 my $balance_due_msg = $self->balance_due_msg;
853 push @buf,['','-----------'];
854 push @buf,[$balance_due_msg, $money_char.
855 sprintf("%10.2f", $balance_due ) ];
858 $template ||= $self->_agent_template;
859 my $templatefile = 'invoice_template';
860 $templatefile .= "_$template" if length($template);
861 my @invoice_template = $conf->config($templatefile)
862 or die "cannot load config file $templatefile";
865 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
866 /invoice_lines\((\d*)\)/;
867 $invoice_lines += $1 || scalar(@buf);
870 die "no invoice_lines() functions in template?" unless $wasfunc;
871 my $invoice_template = new Text::Template (
873 SOURCE => [ map "$_\n", @invoice_template ],
874 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
875 $invoice_template->compile()
876 or die "can't compile template: $Text::Template::ERROR";
878 #setup template variables
879 package FS::cust_bill::_template; #!
880 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
882 $invnum = $self->invnum;
883 $date = $self->_date;
885 $agent = $self->cust_main->agent->agent;
887 if ( $FS::cust_bill::invoice_lines ) {
889 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
891 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
896 #format address (variable for the template)
898 @address = ( '', '', '', '', '', '' );
899 package FS::cust_bill; #!
900 $FS::cust_bill::_template::address[$l++] =
902 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
903 ? " (P.O. #". $cust_main->payinfo. ")"
907 $FS::cust_bill::_template::address[$l++] = $cust_main->company
908 if $cust_main->company;
909 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
910 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
911 if $cust_main->address2;
912 $FS::cust_bill::_template::address[$l++] =
913 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
914 $FS::cust_bill::_template::address[$l++] = $cust_main->country
915 unless $cust_main->country eq 'US';
917 # #overdue? (variable for the template)
918 # $FS::cust_bill::_template::overdue = (
920 # && $today > $self->_date
921 ## && $self->printed > 1
922 # && $self->printed > 0
925 #and subroutine for the template
926 sub FS::cust_bill::_template::invoice_lines {
927 my $lines = shift || scalar(@buf);
929 scalar(@buf) ? shift @buf : [ '', '' ];
935 $FS::cust_bill::_template::page = 1;
939 push @collect, split("\n",
940 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
942 $FS::cust_bill::_template::page++;
945 map "$_\n", @collect;
949 =item print_latex [ TIME [ , TEMPLATE ] ]
951 Internal method - returns a filename of a filled-in LaTeX template for this
952 invoice (Note: add ".tex" to get the actual filename).
954 See print_ps and print_pdf for methods that return PostScript and PDF output.
956 TIME an optional value used to control the printing of overdue messages. The
957 default is now. It isn't the date of the invoice; that's the `_date' field.
958 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
959 L<Time::Local> and L<Date::Parse> for conversion functions.
963 #still some false laziness w/print_text
966 my( $self, $today, $template ) = @_;
969 # my $invnum = $self->invnum;
970 my $cust_main = $self->cust_main;
971 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
972 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
974 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
975 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
976 #my $balance_due = $self->owed + $pr_total - $cr_total;
977 my $balance_due = $self->owed + $pr_total;
980 #my($description,$amount);
984 $template ||= $self->_agent_template;
985 my $templatefile = 'invoice_latex';
986 my $suffix = length($template) ? "_$template" : '';
987 $templatefile .= $suffix;
988 my @invoice_template = $conf->config($templatefile)
989 or die "cannot load config file $templatefile";
992 'invnum' => $self->invnum,
993 'date' => time2str('%b %o, %Y', $self->_date),
994 'agent' => _latex_escape($cust_main->agent->agent),
995 'payname' => _latex_escape($cust_main->payname),
996 'company' => _latex_escape($cust_main->company),
997 'address1' => _latex_escape($cust_main->address1),
998 'address2' => _latex_escape($cust_main->address2),
999 'city' => _latex_escape($cust_main->city),
1000 'state' => _latex_escape($cust_main->state),
1001 'zip' => _latex_escape($cust_main->zip),
1002 'country' => _latex_escape($cust_main->country),
1003 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1004 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1006 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1007 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1010 my $countrydefault = $conf->config('countrydefault') || 'US';
1011 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1013 #do variable substitutions in notes
1014 $invoice_data{'notes'} =
1016 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1017 $conf->config_orbase('invoice_latexnotes', $suffix)
1020 $invoice_data{'footer'} =~ s/\n+$//;
1021 $invoice_data{'smallfooter'} =~ s/\n+$//;
1022 $invoice_data{'notes'} =~ s/\n+$//;
1024 $invoice_data{'po_line'} =
1025 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1026 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1030 my @total_item = ();
1032 while ( @invoice_template ) {
1033 my $line = shift @invoice_template;
1035 if ( $line =~ /^%%Detail\s*$/ ) {
1037 while ( ( my $line_item_line = shift @invoice_template )
1038 !~ /^%%EndDetail\s*$/ ) {
1039 push @line_item, $line_item_line;
1041 foreach my $line_item ( $self->_items ) {
1042 #foreach my $line_item ( $self->_items_pkg ) {
1043 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1044 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1045 if ( exists $line_item->{'ext_description'} ) {
1046 $invoice_data{'description'} .=
1047 "\\tabularnewline\n~~".
1048 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1050 $invoice_data{'amount'} = $line_item->{'amount'};
1051 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1053 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1056 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1058 while ( ( my $total_item_line = shift @invoice_template )
1059 !~ /^%%EndTotalDetails\s*$/ ) {
1060 push @total_item, $total_item_line;
1063 my @total_fill = ();
1066 foreach my $tax ( $self->_items_tax ) {
1067 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1068 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1070 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1075 $invoice_data{'total_item'} = 'Sub-total';
1076 $invoice_data{'total_amount'} =
1077 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1078 unshift @total_fill,
1079 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1083 $invoice_data{'total_item'} = '\textbf{Total}';
1084 $invoice_data{'total_amount'} =
1085 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1087 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1090 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1093 foreach my $credit ( $self->_items_credits ) {
1094 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1096 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1098 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1103 foreach my $payment ( $self->_items_payments ) {
1104 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1106 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1108 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1112 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1113 $invoice_data{'total_amount'} =
1114 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1116 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1119 push @filled_in, @total_fill;
1122 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1123 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1124 push @filled_in, $line;
1135 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1136 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1140 ) or die "can't open temp file: $!\n";
1141 print $fh join("\n", @filled_in ), "\n";
1144 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1149 =item print_ps [ TIME [ , TEMPLATE ] ]
1151 Returns an postscript invoice, as a scalar.
1153 TIME an optional value used to control the printing of overdue messages. The
1154 default is now. It isn't the date of the invoice; that's the `_date' field.
1155 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1156 L<Time::Local> and L<Date::Parse> for conversion functions.
1163 my $file = $self->print_latex(@_);
1165 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1168 my $sfile = shell_quote $file;
1170 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1171 or die "pslatex $file.tex failed: $!";
1172 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1173 or die "pslatex $file.tex failed: $!";
1175 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1176 or die "dvips failed: $!";
1178 open(POSTSCRIPT, "<$file.ps")
1179 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1181 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1184 while (<POSTSCRIPT>) {
1194 =item print_pdf [ TIME [ , TEMPLATE ] ]
1196 Returns an PDF invoice, as a scalar.
1198 TIME an optional value used to control the printing of overdue messages. The
1199 default is now. It isn't the date of the invoice; that's the `_date' field.
1200 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1201 L<Time::Local> and L<Date::Parse> for conversion functions.
1208 my $file = $self->print_latex(@_);
1210 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1213 #system('pdflatex', "$file.tex");
1214 #system('pdflatex', "$file.tex");
1215 #! LaTeX Error: Unknown graphics extension: .eps.
1217 my $sfile = shell_quote $file;
1219 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1220 or die "pslatex $file.tex failed: $!";
1221 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1222 or die "pslatex $file.tex failed: $!";
1224 #system('dvipdf', "$file.dvi", "$file.pdf" );
1226 "dvips -q -t letter -f $sfile.dvi ".
1227 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1230 or die "dvips | gs failed: $!";
1232 open(PDF, "<$file.pdf")
1233 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1235 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1248 # quick subroutine for print_latex
1250 # There are ten characters that LaTeX treats as special characters, which
1251 # means that they do not simply typeset themselves:
1252 # # $ % & ~ _ ^ \ { }
1254 # TeX ignores blanks following an escaped character; if you want a blank (as
1255 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1259 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1263 #utility methods for print_*
1265 sub balance_due_msg {
1267 my $msg = 'Balance Due';
1268 return $msg unless $conf->exists('invoice_default_terms');
1269 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1270 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1271 } elsif ( $conf->config('invoice_default_terms') ) {
1272 $msg .= ' - '. $conf->config('invoice_default_terms');
1279 my @display = scalar(@_)
1281 : qw( _items_previous _items_pkg );
1282 #: qw( _items_pkg );
1283 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1285 foreach my $display ( @display ) {
1286 push @b, $self->$display(@_);
1291 sub _items_previous {
1293 my $cust_main = $self->cust_main;
1294 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1296 foreach ( @pr_cust_bill ) {
1298 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1299 ' ('. time2str('%x',$_->_date). ')',
1300 #'pkgpart' => 'N/A',
1302 'amount' => sprintf("%10.2f", $_->owed),
1308 # 'description' => 'Previous Balance',
1309 # #'pkgpart' => 'N/A',
1310 # 'pkgnum' => 'N/A',
1311 # 'amount' => sprintf("%10.2f", $pr_total ),
1312 # 'ext_description' => [ map {
1313 # "Invoice ". $_->invnum.
1314 # " (". time2str("%x",$_->_date). ") ".
1315 # sprintf("%10.2f", $_->owed)
1316 # } @pr_cust_bill ],
1323 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1324 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1329 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1330 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1333 sub _items_cust_bill_pkg {
1335 my $cust_bill_pkg = shift;
1338 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1340 if ( $cust_bill_pkg->pkgnum ) {
1342 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1343 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1344 my $pkg = $part_pkg->pkg;
1347 #tie %labels, 'Tie::IxHash';
1348 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1349 my @ext_description;
1350 foreach my $label ( keys %labels ) {
1351 my @values = @{ $labels{$label} };
1352 my $num = scalar(@values);
1354 push @ext_description, "$label ($num)";
1356 push @ext_description, map { "$label: $_" } @values;
1360 if ( $cust_bill_pkg->setup != 0 ) {
1361 my $description = $pkg;
1362 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1363 my @d = @ext_description;
1364 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1366 'description' => $description,
1367 #'pkgpart' => $part_pkg->pkgpart,
1368 'pkgnum' => $cust_pkg->pkgnum,
1369 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1370 'ext_description' => \@d,
1374 if ( $cust_bill_pkg->recur != 0 ) {
1376 'description' => "$pkg (" .
1377 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1378 time2str('%x', $cust_bill_pkg->edate). ')',
1379 #'pkgpart' => $part_pkg->pkgpart,
1380 'pkgnum' => $cust_pkg->pkgnum,
1381 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1382 'ext_description' => [ @ext_description,
1383 $cust_bill_pkg->details,
1388 } else { #pkgnum tax or one-shot line item (??)
1390 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1391 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1393 if ( $cust_bill_pkg->setup != 0 ) {
1395 'description' => $itemdesc,
1396 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1399 if ( $cust_bill_pkg->recur != 0 ) {
1401 'description' => "$itemdesc (".
1402 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1403 time2str("%x", $cust_bill_pkg->edate). ')',
1404 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1416 sub _items_credits {
1421 foreach ( $self->cust_credited ) {
1423 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1425 my $reason = $_->cust_credit->reason;
1426 #my $reason = substr($_->cust_credit->reason,0,32);
1427 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1428 $reason = " ($reason) " if $reason;
1430 #'description' => 'Credit ref\#'. $_->crednum.
1431 # " (". time2str("%x",$_->cust_credit->_date) .")".
1433 'description' => 'Credit applied '.
1434 time2str("%x",$_->cust_credit->_date). $reason,
1435 'amount' => sprintf("%10.2f",$_->amount),
1438 #foreach ( @cr_cust_credit ) {
1440 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1441 # $money_char. sprintf("%10.2f",$_->credited)
1449 sub _items_payments {
1453 #get & print payments
1454 foreach ( $self->cust_bill_pay ) {
1456 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1459 'description' => "Payment received ".
1460 time2str("%x",$_->cust_pay->_date ),
1461 'amount' => sprintf("%10.2f", $_->amount )
1475 print_text formatting (and some logic :/) is in source, but needs to be
1476 slurped in from a file. Also number of lines ($=).
1480 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1481 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base