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.
725 my( $self, $today, $template ) = @_;
727 # my $invnum = $self->invnum;
728 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
729 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
730 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
732 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
733 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
734 #my $balance_due = $self->owed + $pr_total - $cr_total;
735 my $balance_due = $self->owed + $pr_total;
738 #my($description,$amount);
742 foreach ( @pr_cust_bill ) {
744 "Previous Balance, Invoice #". $_->invnum.
745 " (". time2str("%x",$_->_date). ")",
746 $money_char. sprintf("%10.2f",$_->owed)
750 push @buf,['','-----------'];
751 push @buf,[ 'Total Previous Balance',
752 $money_char. sprintf("%10.2f",$pr_total ) ];
757 foreach my $cust_bill_pkg (
758 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
759 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
762 if ( $cust_bill_pkg->pkgnum ) {
764 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
765 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
766 my $pkg = $part_pkg->pkg;
768 if ( $cust_bill_pkg->setup != 0 ) {
769 my $description = $pkg;
770 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
771 push @buf, [ $description,
772 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
774 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
777 if ( $cust_bill_pkg->recur != 0 ) {
779 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
780 time2str("%x", $cust_bill_pkg->edate) . ")",
781 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
784 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
787 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
789 } else { #pkgnum tax or one-shot line item
790 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
791 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
793 if ( $cust_bill_pkg->setup != 0 ) {
794 push @buf, [ $itemdesc,
795 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
797 if ( $cust_bill_pkg->recur != 0 ) {
798 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
799 . time2str("%x", $cust_bill_pkg->edate). ")",
800 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
806 push @buf,['','-----------'];
807 push @buf,['Total New Charges',
808 $money_char. sprintf("%10.2f",$self->charged) ];
811 push @buf,['','-----------'];
812 push @buf,['Total Charges',
813 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
817 foreach ( $self->cust_credited ) {
819 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
821 my $reason = substr($_->cust_credit->reason,0,32);
822 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
823 $reason = " ($reason) " if $reason;
825 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
827 $money_char. sprintf("%10.2f",$_->amount)
830 #foreach ( @cr_cust_credit ) {
832 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
833 # $money_char. sprintf("%10.2f",$_->credited)
837 #get & print payments
838 foreach ( $self->cust_bill_pay ) {
840 #something more elaborate if $_->amount ne ->cust_pay->paid ?
843 "Payment received ". time2str("%x",$_->cust_pay->_date ),
844 $money_char. sprintf("%10.2f",$_->amount )
849 my $balance_due_msg = $self->balance_due_msg;
851 push @buf,['','-----------'];
852 push @buf,[$balance_due_msg, $money_char.
853 sprintf("%10.2f", $balance_due ) ];
856 $template ||= $self->_agent_template;
857 my $templatefile = 'invoice_template';
858 $templatefile .= "_$template" if length($template);
859 my @invoice_template = $conf->config($templatefile)
860 or die "cannot load config file $templatefile";
863 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
864 /invoice_lines\((\d*)\)/;
865 $invoice_lines += $1 || scalar(@buf);
868 die "no invoice_lines() functions in template?" unless $wasfunc;
869 my $invoice_template = new Text::Template (
871 SOURCE => [ map "$_\n", @invoice_template ],
872 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
873 $invoice_template->compile()
874 or die "can't compile template: $Text::Template::ERROR";
876 #setup template variables
877 package FS::cust_bill::_template; #!
878 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
880 $invnum = $self->invnum;
881 $date = $self->_date;
883 $agent = $self->cust_main->agent->agent;
885 if ( $FS::cust_bill::invoice_lines ) {
887 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
889 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
894 #format address (variable for the template)
896 @address = ( '', '', '', '', '', '' );
897 package FS::cust_bill; #!
898 $FS::cust_bill::_template::address[$l++] =
900 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
901 ? " (P.O. #". $cust_main->payinfo. ")"
905 $FS::cust_bill::_template::address[$l++] = $cust_main->company
906 if $cust_main->company;
907 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
908 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
909 if $cust_main->address2;
910 $FS::cust_bill::_template::address[$l++] =
911 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
912 $FS::cust_bill::_template::address[$l++] = $cust_main->country
913 unless $cust_main->country eq 'US';
915 # #overdue? (variable for the template)
916 # $FS::cust_bill::_template::overdue = (
918 # && $today > $self->_date
919 ## && $self->printed > 1
920 # && $self->printed > 0
923 #and subroutine for the template
924 sub FS::cust_bill::_template::invoice_lines {
925 my $lines = shift || scalar(@buf);
927 scalar(@buf) ? shift @buf : [ '', '' ];
933 $FS::cust_bill::_template::page = 1;
937 push @collect, split("\n",
938 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
940 $FS::cust_bill::_template::page++;
943 map "$_\n", @collect;
947 =item print_latex [ TIME [ , TEMPLATE ] ]
949 Internal method - returns a filename of a filled-in LaTeX template for this
950 invoice (Note: add ".tex" to get the actual filename).
952 See print_ps and print_pdf for methods that return PostScript and PDF output.
954 TIME an optional value used to control the printing of overdue messages. The
955 default is now. It isn't the date of the invoice; that's the `_date' field.
956 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
957 L<Time::Local> and L<Date::Parse> for conversion functions.
961 #still some false laziness w/print_text
964 my( $self, $today, $template ) = @_;
967 # my $invnum = $self->invnum;
968 my $cust_main = $self->cust_main;
969 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
970 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
972 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
973 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
974 #my $balance_due = $self->owed + $pr_total - $cr_total;
975 my $balance_due = $self->owed + $pr_total;
978 #my($description,$amount);
982 $template ||= $self->_agent_template;
983 my $templatefile = 'invoice_latex';
984 my $suffix = length($template) ? "_$template" : '';
985 $templatefile .= $suffix;
986 my @invoice_template = $conf->config($templatefile)
987 or die "cannot load config file $templatefile";
990 'invnum' => $self->invnum,
991 'date' => time2str('%b %o, %Y', $self->_date),
992 'agent' => _latex_escape($cust_main->agent->agent),
993 'payname' => _latex_escape($cust_main->payname),
994 'company' => _latex_escape($cust_main->company),
995 'address1' => _latex_escape($cust_main->address1),
996 'address2' => _latex_escape($cust_main->address2),
997 'city' => _latex_escape($cust_main->city),
998 'state' => _latex_escape($cust_main->state),
999 'zip' => _latex_escape($cust_main->zip),
1000 'country' => _latex_escape($cust_main->country),
1001 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1002 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1004 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1005 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1008 my $countrydefault = $conf->config('countrydefault') || 'US';
1009 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1011 #do variable substitutions in notes
1012 $invoice_data{'notes'} =
1014 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1015 $conf->config_orbase('invoice_latexnotes', $suffix)
1018 $invoice_data{'footer'} =~ s/\n+$//;
1019 $invoice_data{'smallfooter'} =~ s/\n+$//;
1020 $invoice_data{'notes'} =~ s/\n+$//;
1022 $invoice_data{'po_line'} =
1023 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1024 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1028 my @total_item = ();
1030 while ( @invoice_template ) {
1031 my $line = shift @invoice_template;
1033 if ( $line =~ /^%%Detail\s*$/ ) {
1035 while ( ( my $line_item_line = shift @invoice_template )
1036 !~ /^%%EndDetail\s*$/ ) {
1037 push @line_item, $line_item_line;
1039 foreach my $line_item ( $self->_items ) {
1040 #foreach my $line_item ( $self->_items_pkg ) {
1041 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1042 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1043 if ( exists $line_item->{'ext_description'} ) {
1044 $invoice_data{'description'} .=
1045 "\\tabularnewline\n~~".
1046 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1048 $invoice_data{'amount'} = $line_item->{'amount'};
1049 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1051 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1054 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1056 while ( ( my $total_item_line = shift @invoice_template )
1057 !~ /^%%EndTotalDetails\s*$/ ) {
1058 push @total_item, $total_item_line;
1061 my @total_fill = ();
1064 foreach my $tax ( $self->_items_tax ) {
1065 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1066 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1068 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1073 $invoice_data{'total_item'} = 'Sub-total';
1074 $invoice_data{'total_amount'} =
1075 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1076 unshift @total_fill,
1077 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1081 $invoice_data{'total_item'} = '\textbf{Total}';
1082 $invoice_data{'total_amount'} =
1083 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1085 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1088 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1091 foreach my $credit ( $self->_items_credits ) {
1092 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1094 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1096 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1101 foreach my $payment ( $self->_items_payments ) {
1102 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1104 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1106 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1110 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1111 $invoice_data{'total_amount'} =
1112 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1114 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1117 push @filled_in, @total_fill;
1120 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1121 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1122 push @filled_in, $line;
1133 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1134 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1138 ) or die "can't open temp file: $!\n";
1139 print $fh join("\n", @filled_in ), "\n";
1142 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1147 =item print_ps [ TIME [ , TEMPLATE ] ]
1149 Returns an postscript invoice, as a scalar.
1151 TIME an optional value used to control the printing of overdue messages. The
1152 default is now. It isn't the date of the invoice; that's the `_date' field.
1153 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1154 L<Time::Local> and L<Date::Parse> for conversion functions.
1161 my $file = $self->print_latex(@_);
1163 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1166 my $sfile = shell_quote $file;
1168 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1169 or die "pslatex $file.tex failed: $!";
1170 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1171 or die "pslatex $file.tex failed: $!";
1173 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1174 or die "dvips failed: $!";
1176 open(POSTSCRIPT, "<$file.ps")
1177 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1179 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1182 while (<POSTSCRIPT>) {
1192 =item print_pdf [ TIME [ , TEMPLATE ] ]
1194 Returns an PDF invoice, as a scalar.
1196 TIME an optional value used to control the printing of overdue messages. The
1197 default is now. It isn't the date of the invoice; that's the `_date' field.
1198 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1199 L<Time::Local> and L<Date::Parse> for conversion functions.
1206 my $file = $self->print_latex(@_);
1208 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1211 #system('pdflatex', "$file.tex");
1212 #system('pdflatex', "$file.tex");
1213 #! LaTeX Error: Unknown graphics extension: .eps.
1215 my $sfile = shell_quote $file;
1217 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1218 or die "pslatex $file.tex failed: $!";
1219 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1220 or die "pslatex $file.tex failed: $!";
1222 #system('dvipdf', "$file.dvi", "$file.pdf" );
1224 "dvips -q -t letter -f $sfile.dvi ".
1225 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1228 or die "dvips | gs failed: $!";
1230 open(PDF, "<$file.pdf")
1231 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1233 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1246 # quick subroutine for print_latex
1248 # There are ten characters that LaTeX treats as special characters, which
1249 # means that they do not simply typeset themselves:
1250 # # $ % & ~ _ ^ \ { }
1252 # TeX ignores blanks following an escaped character; if you want a blank (as
1253 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1257 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1261 #utility methods for print_*
1263 sub balance_due_msg {
1265 my $msg = 'Balance Due';
1266 return $msg unless $conf->exists('invoice_default_terms');
1267 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1268 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1269 } elsif ( $conf->config('invoice_default_terms') ) {
1270 $msg .= ' - '. $conf->config('invoice_default_terms');
1277 my @display = scalar(@_)
1279 : qw( _items_previous _items_pkg );
1280 #: qw( _items_pkg );
1281 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1283 foreach my $display ( @display ) {
1284 push @b, $self->$display(@_);
1289 sub _items_previous {
1291 my $cust_main = $self->cust_main;
1292 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1294 foreach ( @pr_cust_bill ) {
1296 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1297 ' ('. time2str('%x',$_->_date). ')',
1298 #'pkgpart' => 'N/A',
1300 'amount' => sprintf("%10.2f", $_->owed),
1306 # 'description' => 'Previous Balance',
1307 # #'pkgpart' => 'N/A',
1308 # 'pkgnum' => 'N/A',
1309 # 'amount' => sprintf("%10.2f", $pr_total ),
1310 # 'ext_description' => [ map {
1311 # "Invoice ". $_->invnum.
1312 # " (". time2str("%x",$_->_date). ") ".
1313 # sprintf("%10.2f", $_->owed)
1314 # } @pr_cust_bill ],
1321 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1322 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1327 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1328 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1331 sub _items_cust_bill_pkg {
1333 my $cust_bill_pkg = shift;
1336 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1338 if ( $cust_bill_pkg->pkgnum ) {
1340 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1341 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1342 my $pkg = $part_pkg->pkg;
1345 #tie %labels, 'Tie::IxHash';
1346 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1347 my @ext_description;
1348 foreach my $label ( keys %labels ) {
1349 my @values = @{ $labels{$label} };
1350 my $num = scalar(@values);
1352 push @ext_description, "$label ($num)";
1354 push @ext_description, map { "$label: $_" } @values;
1358 if ( $cust_bill_pkg->setup != 0 ) {
1359 my $description = $pkg;
1360 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1361 my @d = @ext_description;
1362 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1364 'description' => $description,
1365 #'pkgpart' => $part_pkg->pkgpart,
1366 'pkgnum' => $cust_pkg->pkgnum,
1367 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1368 'ext_description' => \@d,
1372 if ( $cust_bill_pkg->recur != 0 ) {
1374 'description' => "$pkg (" .
1375 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1376 time2str('%x', $cust_bill_pkg->edate). ')',
1377 #'pkgpart' => $part_pkg->pkgpart,
1378 'pkgnum' => $cust_pkg->pkgnum,
1379 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1380 'ext_description' => [ @ext_description,
1381 $cust_bill_pkg->details,
1386 } else { #pkgnum tax or one-shot line item (??)
1388 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1389 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1391 if ( $cust_bill_pkg->setup != 0 ) {
1393 'description' => $itemdesc,
1394 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1397 if ( $cust_bill_pkg->recur != 0 ) {
1399 'description' => "$itemdesc (".
1400 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1401 time2str("%x", $cust_bill_pkg->edate). ')',
1402 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1414 sub _items_credits {
1419 foreach ( $self->cust_credited ) {
1421 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1423 my $reason = $_->cust_credit->reason;
1424 #my $reason = substr($_->cust_credit->reason,0,32);
1425 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1426 $reason = " ($reason) " if $reason;
1428 #'description' => 'Credit ref\#'. $_->crednum.
1429 # " (". time2str("%x",$_->cust_credit->_date) .")".
1431 'description' => 'Credit applied '.
1432 time2str("%x",$_->cust_credit->_date). $reason,
1433 'amount' => sprintf("%10.2f",$_->amount),
1436 #foreach ( @cr_cust_credit ) {
1438 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1439 # $money_char. sprintf("%10.2f",$_->credited)
1447 sub _items_payments {
1451 #get & print payments
1452 foreach ( $self->cust_bill_pay ) {
1454 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1457 'description' => "Payment received ".
1458 time2str("%x",$_->cust_pay->_date ),
1459 'amount' => sprintf("%10.2f", $_->amount )
1473 print_text formatting (and some logic :/) is in source, but needs to be
1474 slurped in from a file. Also number of lines ($=).
1478 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1479 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base