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 $cust_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 $cust_bill_event;
702 if ( $cust_bill_event->plandata =~ /^$option (.*)$/m ) {
705 warn "can't parse plandata for $1";
711 =item print_text [ TIME [ , TEMPLATE ] ]
713 Returns an text invoice, as a list of lines.
715 TIME an optional value used to control the printing of overdue messages. The
716 default is now. It isn't the date of the invoice; that's the `_date' field.
717 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
718 L<Time::Local> and L<Date::Parse> for conversion functions.
724 my( $self, $today, $template ) = @_;
726 # my $invnum = $self->invnum;
727 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
728 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
729 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
731 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
732 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
733 #my $balance_due = $self->owed + $pr_total - $cr_total;
734 my $balance_due = $self->owed + $pr_total;
737 #my($description,$amount);
741 foreach ( @pr_cust_bill ) {
743 "Previous Balance, Invoice #". $_->invnum.
744 " (". time2str("%x",$_->_date). ")",
745 $money_char. sprintf("%10.2f",$_->owed)
749 push @buf,['','-----------'];
750 push @buf,[ 'Total Previous Balance',
751 $money_char. sprintf("%10.2f",$pr_total ) ];
756 foreach my $cust_bill_pkg (
757 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
758 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
761 if ( $cust_bill_pkg->pkgnum ) {
763 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
764 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
765 my $pkg = $part_pkg->pkg;
767 if ( $cust_bill_pkg->setup != 0 ) {
768 my $description = $pkg;
769 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
770 push @buf, [ $description,
771 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
773 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
776 if ( $cust_bill_pkg->recur != 0 ) {
778 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
779 time2str("%x", $cust_bill_pkg->edate) . ")",
780 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
783 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
786 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
788 } else { #pkgnum tax or one-shot line item
789 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
790 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
792 if ( $cust_bill_pkg->setup != 0 ) {
793 push @buf, [ $itemdesc,
794 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
796 if ( $cust_bill_pkg->recur != 0 ) {
797 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
798 . time2str("%x", $cust_bill_pkg->edate). ")",
799 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
805 push @buf,['','-----------'];
806 push @buf,['Total New Charges',
807 $money_char. sprintf("%10.2f",$self->charged) ];
810 push @buf,['','-----------'];
811 push @buf,['Total Charges',
812 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
816 foreach ( $self->cust_credited ) {
818 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
820 my $reason = substr($_->cust_credit->reason,0,32);
821 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
822 $reason = " ($reason) " if $reason;
824 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
826 $money_char. sprintf("%10.2f",$_->amount)
829 #foreach ( @cr_cust_credit ) {
831 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
832 # $money_char. sprintf("%10.2f",$_->credited)
836 #get & print payments
837 foreach ( $self->cust_bill_pay ) {
839 #something more elaborate if $_->amount ne ->cust_pay->paid ?
842 "Payment received ". time2str("%x",$_->cust_pay->_date ),
843 $money_char. sprintf("%10.2f",$_->amount )
848 my $balance_due_msg = $self->balance_due_msg;
850 push @buf,['','-----------'];
851 push @buf,[$balance_due_msg, $money_char.
852 sprintf("%10.2f", $balance_due ) ];
855 $template ||= $self->_agent_template;
856 my $templatefile = 'invoice_template';
857 $templatefile .= "_$template" if length($template);
858 my @invoice_template = $conf->config($templatefile)
859 or die "cannot load config file $templatefile";
862 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
863 /invoice_lines\((\d*)\)/;
864 $invoice_lines += $1 || scalar(@buf);
867 die "no invoice_lines() functions in template?" unless $wasfunc;
868 my $invoice_template = new Text::Template (
870 SOURCE => [ map "$_\n", @invoice_template ],
871 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
872 $invoice_template->compile()
873 or die "can't compile template: $Text::Template::ERROR";
875 #setup template variables
876 package FS::cust_bill::_template; #!
877 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
879 $invnum = $self->invnum;
880 $date = $self->_date;
882 $agent = $self->cust_main->agent->agent;
884 if ( $FS::cust_bill::invoice_lines ) {
886 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
888 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
893 #format address (variable for the template)
895 @address = ( '', '', '', '', '', '' );
896 package FS::cust_bill; #!
897 $FS::cust_bill::_template::address[$l++] =
899 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
900 ? " (P.O. #". $cust_main->payinfo. ")"
904 $FS::cust_bill::_template::address[$l++] = $cust_main->company
905 if $cust_main->company;
906 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
907 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
908 if $cust_main->address2;
909 $FS::cust_bill::_template::address[$l++] =
910 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
911 $FS::cust_bill::_template::address[$l++] = $cust_main->country
912 unless $cust_main->country eq 'US';
914 # #overdue? (variable for the template)
915 # $FS::cust_bill::_template::overdue = (
917 # && $today > $self->_date
918 ## && $self->printed > 1
919 # && $self->printed > 0
922 #and subroutine for the template
923 sub FS::cust_bill::_template::invoice_lines {
924 my $lines = shift || scalar(@buf);
926 scalar(@buf) ? shift @buf : [ '', '' ];
932 $FS::cust_bill::_template::page = 1;
936 push @collect, split("\n",
937 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
939 $FS::cust_bill::_template::page++;
942 map "$_\n", @collect;
946 =item print_latex [ TIME [ , TEMPLATE ] ]
948 Internal method - returns a filename of a filled-in LaTeX template for this
949 invoice (Note: add ".tex" to get the actual filename).
951 See print_ps and print_pdf for methods that return PostScript and PDF output.
953 TIME an optional value used to control the printing of overdue messages. The
954 default is now. It isn't the date of the invoice; that's the `_date' field.
955 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
956 L<Time::Local> and L<Date::Parse> for conversion functions.
960 #still some false laziness w/print_text
963 my( $self, $today, $template ) = @_;
966 # my $invnum = $self->invnum;
967 my $cust_main = $self->cust_main;
968 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
969 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
971 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
972 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
973 #my $balance_due = $self->owed + $pr_total - $cr_total;
974 my $balance_due = $self->owed + $pr_total;
977 #my($description,$amount);
981 $template ||= $self->_agent_template;
982 my $templatefile = 'invoice_latex';
983 my $suffix = length($template) ? "_$template" : '';
984 $templatefile .= $suffix;
985 my @invoice_template = $conf->config($templatefile)
986 or die "cannot load config file $templatefile";
989 'invnum' => $self->invnum,
990 'date' => time2str('%b %o, %Y', $self->_date),
991 'agent' => _latex_escape($cust_main->agent->agent),
992 'payname' => _latex_escape($cust_main->payname),
993 'company' => _latex_escape($cust_main->company),
994 'address1' => _latex_escape($cust_main->address1),
995 'address2' => _latex_escape($cust_main->address2),
996 'city' => _latex_escape($cust_main->city),
997 'state' => _latex_escape($cust_main->state),
998 'zip' => _latex_escape($cust_main->zip),
999 'country' => _latex_escape($cust_main->country),
1000 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1001 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1003 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1004 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1007 my $countrydefault = $conf->config('countrydefault') || 'US';
1008 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1010 #do variable substitutions in notes
1011 $invoice_data{'notes'} =
1013 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1014 $conf->config_orbase('invoice_latexnotes', $suffix)
1017 $invoice_data{'footer'} =~ s/\n+$//;
1018 $invoice_data{'smallfooter'} =~ s/\n+$//;
1019 $invoice_data{'notes'} =~ s/\n+$//;
1021 $invoice_data{'po_line'} =
1022 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1023 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1027 my @total_item = ();
1029 while ( @invoice_template ) {
1030 my $line = shift @invoice_template;
1032 if ( $line =~ /^%%Detail\s*$/ ) {
1034 while ( ( my $line_item_line = shift @invoice_template )
1035 !~ /^%%EndDetail\s*$/ ) {
1036 push @line_item, $line_item_line;
1038 foreach my $line_item ( $self->_items ) {
1039 #foreach my $line_item ( $self->_items_pkg ) {
1040 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1041 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1042 if ( exists $line_item->{'ext_description'} ) {
1043 $invoice_data{'description'} .=
1044 "\\tabularnewline\n~~".
1045 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1047 $invoice_data{'amount'} = $line_item->{'amount'};
1048 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1050 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1053 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1055 while ( ( my $total_item_line = shift @invoice_template )
1056 !~ /^%%EndTotalDetails\s*$/ ) {
1057 push @total_item, $total_item_line;
1060 my @total_fill = ();
1063 foreach my $tax ( $self->_items_tax ) {
1064 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1065 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1067 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1072 $invoice_data{'total_item'} = 'Sub-total';
1073 $invoice_data{'total_amount'} =
1074 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1075 unshift @total_fill,
1076 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1080 $invoice_data{'total_item'} = '\textbf{Total}';
1081 $invoice_data{'total_amount'} =
1082 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1084 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1087 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1090 foreach my $credit ( $self->_items_credits ) {
1091 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1093 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1095 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1100 foreach my $payment ( $self->_items_payments ) {
1101 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1103 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1105 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1109 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1110 $invoice_data{'total_amount'} =
1111 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1113 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1116 push @filled_in, @total_fill;
1119 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1120 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1121 push @filled_in, $line;
1132 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1133 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1137 ) or die "can't open temp file: $!\n";
1138 print $fh join("\n", @filled_in ), "\n";
1141 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1146 =item print_ps [ TIME [ , TEMPLATE ] ]
1148 Returns an postscript invoice, as a scalar.
1150 TIME an optional value used to control the printing of overdue messages. The
1151 default is now. It isn't the date of the invoice; that's the `_date' field.
1152 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1153 L<Time::Local> and L<Date::Parse> for conversion functions.
1160 my $file = $self->print_latex(@_);
1162 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1165 my $sfile = shell_quote $file;
1167 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1168 or die "pslatex $file.tex failed: $!";
1169 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1170 or die "pslatex $file.tex failed: $!";
1172 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1173 or die "dvips failed: $!";
1175 open(POSTSCRIPT, "<$file.ps")
1176 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1178 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1181 while (<POSTSCRIPT>) {
1191 =item print_pdf [ TIME [ , TEMPLATE ] ]
1193 Returns an PDF invoice, as a scalar.
1195 TIME an optional value used to control the printing of overdue messages. The
1196 default is now. It isn't the date of the invoice; that's the `_date' field.
1197 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1198 L<Time::Local> and L<Date::Parse> for conversion functions.
1205 my $file = $self->print_latex(@_);
1207 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1210 #system('pdflatex', "$file.tex");
1211 #system('pdflatex', "$file.tex");
1212 #! LaTeX Error: Unknown graphics extension: .eps.
1214 my $sfile = shell_quote $file;
1216 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1217 or die "pslatex $file.tex failed: $!";
1218 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1219 or die "pslatex $file.tex failed: $!";
1221 #system('dvipdf', "$file.dvi", "$file.pdf" );
1223 "dvips -q -t letter -f $sfile.dvi ".
1224 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1227 or die "dvips | gs failed: $!";
1229 open(PDF, "<$file.pdf")
1230 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1232 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1245 # quick subroutine for print_latex
1247 # There are ten characters that LaTeX treats as special characters, which
1248 # means that they do not simply typeset themselves:
1249 # # $ % & ~ _ ^ \ { }
1251 # TeX ignores blanks following an escaped character; if you want a blank (as
1252 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1256 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1260 #utility methods for print_*
1262 sub balance_due_msg {
1264 my $msg = 'Balance Due';
1265 return $msg unless $conf->exists('invoice_default_terms');
1266 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1267 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1268 } elsif ( $conf->config('invoice_default_terms') ) {
1269 $msg .= ' - '. $conf->config('invoice_default_terms');
1276 my @display = scalar(@_)
1278 : qw( _items_previous _items_pkg );
1279 #: qw( _items_pkg );
1280 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1282 foreach my $display ( @display ) {
1283 push @b, $self->$display(@_);
1288 sub _items_previous {
1290 my $cust_main = $self->cust_main;
1291 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1293 foreach ( @pr_cust_bill ) {
1295 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1296 ' ('. time2str('%x',$_->_date). ')',
1297 #'pkgpart' => 'N/A',
1299 'amount' => sprintf("%10.2f", $_->owed),
1305 # 'description' => 'Previous Balance',
1306 # #'pkgpart' => 'N/A',
1307 # 'pkgnum' => 'N/A',
1308 # 'amount' => sprintf("%10.2f", $pr_total ),
1309 # 'ext_description' => [ map {
1310 # "Invoice ". $_->invnum.
1311 # " (". time2str("%x",$_->_date). ") ".
1312 # sprintf("%10.2f", $_->owed)
1313 # } @pr_cust_bill ],
1320 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1321 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1326 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1327 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1330 sub _items_cust_bill_pkg {
1332 my $cust_bill_pkg = shift;
1335 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1337 if ( $cust_bill_pkg->pkgnum ) {
1339 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1340 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1341 my $pkg = $part_pkg->pkg;
1344 #tie %labels, 'Tie::IxHash';
1345 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1346 my @ext_description;
1347 foreach my $label ( keys %labels ) {
1348 my @values = @{ $labels{$label} };
1349 my $num = scalar(@values);
1351 push @ext_description, "$label ($num)";
1353 push @ext_description, map { "$label: $_" } @values;
1357 if ( $cust_bill_pkg->setup != 0 ) {
1358 my $description = $pkg;
1359 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1360 my @d = @ext_description;
1361 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1363 'description' => $description,
1364 #'pkgpart' => $part_pkg->pkgpart,
1365 'pkgnum' => $cust_pkg->pkgnum,
1366 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1367 'ext_description' => \@d,
1371 if ( $cust_bill_pkg->recur != 0 ) {
1373 'description' => "$pkg (" .
1374 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1375 time2str('%x', $cust_bill_pkg->edate). ')',
1376 #'pkgpart' => $part_pkg->pkgpart,
1377 'pkgnum' => $cust_pkg->pkgnum,
1378 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1379 'ext_description' => [ @ext_description,
1380 $cust_bill_pkg->details,
1385 } else { #pkgnum tax or one-shot line item (??)
1387 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1388 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1390 if ( $cust_bill_pkg->setup != 0 ) {
1392 'description' => $itemdesc,
1393 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1396 if ( $cust_bill_pkg->recur != 0 ) {
1398 'description' => "$itemdesc (".
1399 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1400 time2str("%x", $cust_bill_pkg->edate). ')',
1401 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1413 sub _items_credits {
1418 foreach ( $self->cust_credited ) {
1420 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1422 my $reason = $_->cust_credit->reason;
1423 #my $reason = substr($_->cust_credit->reason,0,32);
1424 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1425 $reason = " ($reason) " if $reason;
1427 #'description' => 'Credit ref\#'. $_->crednum.
1428 # " (". time2str("%x",$_->cust_credit->_date) .")".
1430 'description' => 'Credit applied '.
1431 time2str("%x",$_->cust_credit->_date). $reason,
1432 'amount' => sprintf("%10.2f",$_->amount),
1435 #foreach ( @cr_cust_credit ) {
1437 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1438 # $money_char. sprintf("%10.2f",$_->credited)
1446 sub _items_payments {
1450 #get & print payments
1451 foreach ( $self->cust_bill_pay ) {
1453 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1456 'description' => "Payment received ".
1457 time2str("%x",$_->cust_pay->_date ),
1458 'amount' => sprintf("%10.2f", $_->amount )
1472 print_text formatting (and some logic :/) is in source, but needs to be
1473 slurped in from a file. Also number of lines ($=).
1477 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1478 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base