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;
359 #die "$error\n" if $error;
363 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
364 @print_text = $self->print_ps('', $template)
365 if $conf->config('invoice_latex');
366 my $lpr = $conf->config('lpr');
368 or die "Can't open pipe to $lpr: $!\n";
369 print LPR @print_text;
371 or die $! ? "Error closing $lpr: $!\n"
372 : "Exit status $? from $lpr\n";
379 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
381 Like B<send>, but only sends the invoice if it is the newest open invoice for
391 grep { $_->owed > 0 }
392 qsearch('cust_bill', {
393 'custnum' => $self->custnum,
394 #'_date' => { op=>'>', value=>$self->_date },
395 'invnum' => { op=>'>', value=>$self->invnum },
402 =item send_csv OPTIONS
404 Sends invoice as a CSV data-file to a remote host with the specified protocol.
408 protocol - currently only "ftp"
414 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
415 and YYMMDDHHMMSS is a timestamp.
417 The fields of the CSV file is as follows:
419 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
423 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
425 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
426 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
427 fields are filled in.
429 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
430 first two fields (B<record_type> and B<invnum>) and the last five fields
431 (B<pkg> through B<edate>) are filled in.
433 =item invnum - invoice number
435 =item custnum - customer number
437 =item _date - invoice date
439 =item charged - total invoice amount
441 =item first - customer first name
443 =item last - customer first name
445 =item company - company name
447 =item address1 - address line 1
449 =item address2 - address line 1
459 =item pkg - line item description
461 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
463 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
465 =item sdate - start date for recurring fee
467 =item edate - end date for recurring fee
474 my($self, %opt) = @_;
476 #part one: create file
478 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
479 mkdir $spooldir, 0700 unless -d $spooldir;
481 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
483 open(CSV, ">$file") or die "can't open $file: $!";
485 eval "use Text::CSV_XS";
488 my $csv = Text::CSV_XS->new({'always_quote'=>1});
490 my $cust_main = $self->cust_main;
496 time2str("%x", $self->_date),
497 sprintf("%.2f", $self->charged),
498 ( map { $cust_main->getfield($_) }
499 qw( first last company address1 address2 city state zip country ) ),
501 ) or die "can't create csv";
502 print CSV $csv->string. "\n";
504 #new charges (false laziness w/print_text)
505 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
507 my($pkg, $setup, $recur, $sdate, $edate);
508 if ( $cust_bill_pkg->pkgnum ) {
510 ($pkg, $setup, $recur, $sdate, $edate) = (
511 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
512 ( $cust_bill_pkg->setup != 0
513 ? sprintf("%.2f", $cust_bill_pkg->setup )
515 ( $cust_bill_pkg->recur != 0
516 ? sprintf("%.2f", $cust_bill_pkg->recur )
518 time2str("%x", $cust_bill_pkg->sdate),
519 time2str("%x", $cust_bill_pkg->edate),
523 next unless $cust_bill_pkg->setup != 0;
524 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
525 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
527 ($pkg, $setup, $recur, $sdate, $edate) =
528 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
534 ( map { '' } (1..11) ),
535 ($pkg, $setup, $recur, $sdate, $edate)
536 ) or die "can't create csv";
537 print CSV $csv->string. "\n";
541 close CSV or die "can't close CSV: $!";
546 if ( $opt{protocol} eq 'ftp' ) {
547 eval "use Net::FTP;";
549 $net = Net::FTP->new($opt{server}) or die @$;
551 die "unknown protocol: $opt{protocol}";
554 $net->login( $opt{username}, $opt{password} )
555 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
557 $net->binary or die "can't set binary mode";
559 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
561 $net->put($file) or die "can't put $file: $!";
571 Pays this invoice with a compliemntary payment. If there is an error,
572 returns the error, otherwise returns false.
578 my $cust_pay = new FS::cust_pay ( {
579 'invnum' => $self->invnum,
580 'paid' => $self->owed,
583 'payinfo' => $self->cust_main->payinfo,
591 Attempts to pay this invoice with a credit card payment via a
592 Business::OnlinePayment realtime gateway. See
593 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
594 for supported processors.
600 $self->realtime_bop( 'CC', @_ );
605 Attempts to pay this invoice with an electronic check (ACH) payment via a
606 Business::OnlinePayment realtime gateway. See
607 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
608 for supported processors.
614 $self->realtime_bop( 'ECHECK', @_ );
619 Attempts to pay this invoice with phone bill (LEC) payment via a
620 Business::OnlinePayment realtime gateway. See
621 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
622 for supported processors.
628 $self->realtime_bop( 'LEC', @_ );
632 my( $self, $method ) = @_;
634 my $cust_main = $self->cust_main;
635 my $balance = $cust_main->balance;
636 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
637 $amount = sprintf("%.2f", $amount);
638 return "not run (balance $balance)" unless $amount > 0;
640 my $description = 'Internet Services';
641 if ( $conf->exists('business-onlinepayment-description') ) {
642 my $dtempl = $conf->config('business-onlinepayment-description');
644 my $agent_obj = $cust_main->agent
645 or die "can't retreive agent for $cust_main (agentnum ".
646 $cust_main->agentnum. ")";
647 my $agent = $agent_obj->agent;
648 my $pkgs = join(', ',
649 map { $_->cust_pkg->part_pkg->pkg }
650 grep { $_->pkgnum } $self->cust_bill_pkg
652 $description = eval qq("$dtempl");
655 $cust_main->realtime_bop($method, $amount,
656 'description' => $description,
657 'invnum' => $self->invnum,
664 Adds a payment for this invoice to the pending credit card batch (see
665 L<FS::cust_pay_batch>).
671 my $cust_main = $self->cust_main;
673 my $cust_pay_batch = new FS::cust_pay_batch ( {
674 'invnum' => $self->getfield('invnum'),
675 'custnum' => $cust_main->getfield('custnum'),
676 'last' => $cust_main->getfield('last'),
677 'first' => $cust_main->getfield('first'),
678 'address1' => $cust_main->getfield('address1'),
679 'address2' => $cust_main->getfield('address2'),
680 'city' => $cust_main->getfield('city'),
681 'state' => $cust_main->getfield('state'),
682 'zip' => $cust_main->getfield('zip'),
683 'country' => $cust_main->getfield('country'),
684 'cardnum' => $cust_main->getfield('payinfo'),
685 'exp' => $cust_main->getfield('paydate'),
686 'payname' => $cust_main->getfield('payname'),
687 'amount' => $self->owed,
689 my $error = $cust_pay_batch->insert;
690 die $error if $error;
695 sub _agent_template {
697 $self->_agent_plandata('agent_templatename');
700 sub _agent_invoice_from {
702 $self->_agent_plandata('agent_invoice_from');
705 sub _agent_plandata {
706 my( $self, $option ) = @_;
708 my $part_bill_event = qsearchs( 'part_bill_event',
710 'payby' => $self->cust_main->payby,
711 'plan' => 'send_agent',
712 'plandata' => { 'op' => '~',
713 'value' => "(^|\n)agentnum ".
714 $self->cust_main->agentnum.
719 'ORDER BY seconds LIMIT 1'
722 return '' unless $part_bill_event;
724 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
727 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
728 " plandata for $option";
734 =item print_text [ TIME [ , TEMPLATE ] ]
736 Returns an text invoice, as a list of lines.
738 TIME an optional value used to control the printing of overdue messages. The
739 default is now. It isn't the date of the invoice; that's the `_date' field.
740 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
741 L<Time::Local> and L<Date::Parse> for conversion functions.
745 #still some false laziness w/print_text
748 my( $self, $today, $template ) = @_;
751 # my $invnum = $self->invnum;
752 my $cust_main = $self->cust_main;
753 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
754 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
756 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
757 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
758 #my $balance_due = $self->owed + $pr_total - $cr_total;
759 my $balance_due = $self->owed + $pr_total;
762 #my($description,$amount);
766 foreach ( @pr_cust_bill ) {
768 "Previous Balance, Invoice #". $_->invnum.
769 " (". time2str("%x",$_->_date). ")",
770 $money_char. sprintf("%10.2f",$_->owed)
774 push @buf,['','-----------'];
775 push @buf,[ 'Total Previous Balance',
776 $money_char. sprintf("%10.2f",$pr_total ) ];
781 foreach my $cust_bill_pkg (
782 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
783 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
786 if ( $cust_bill_pkg->pkgnum ) {
788 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
789 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
790 my $pkg = $part_pkg->pkg;
792 if ( $cust_bill_pkg->setup != 0 ) {
793 my $description = $pkg;
794 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
795 push @buf, [ $description,
796 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
798 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
799 $cust_pkg->h_labels($self->_date);
802 if ( $cust_bill_pkg->recur != 0 ) {
804 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
805 time2str("%x", $cust_bill_pkg->edate) . ")",
806 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
809 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
810 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
813 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
815 } else { #pkgnum tax or one-shot line item
816 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
817 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
819 if ( $cust_bill_pkg->setup != 0 ) {
820 push @buf, [ $itemdesc,
821 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
823 if ( $cust_bill_pkg->recur != 0 ) {
824 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
825 . time2str("%x", $cust_bill_pkg->edate). ")",
826 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
832 push @buf,['','-----------'];
833 push @buf,['Total New Charges',
834 $money_char. sprintf("%10.2f",$self->charged) ];
837 push @buf,['','-----------'];
838 push @buf,['Total Charges',
839 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
843 foreach ( $self->cust_credited ) {
845 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
847 my $reason = substr($_->cust_credit->reason,0,32);
848 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
849 $reason = " ($reason) " if $reason;
851 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
853 $money_char. sprintf("%10.2f",$_->amount)
856 #foreach ( @cr_cust_credit ) {
858 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
859 # $money_char. sprintf("%10.2f",$_->credited)
863 #get & print payments
864 foreach ( $self->cust_bill_pay ) {
866 #something more elaborate if $_->amount ne ->cust_pay->paid ?
869 "Payment received ". time2str("%x",$_->cust_pay->_date ),
870 $money_char. sprintf("%10.2f",$_->amount )
875 my $balance_due_msg = $self->balance_due_msg;
877 push @buf,['','-----------'];
878 push @buf,[$balance_due_msg, $money_char.
879 sprintf("%10.2f", $balance_due ) ];
882 $template ||= $self->_agent_template;
883 my $templatefile = 'invoice_template';
884 $templatefile .= "_$template" if length($template);
885 my @invoice_template = $conf->config($templatefile)
886 or die "cannot load config file $templatefile";
889 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
890 /invoice_lines\((\d*)\)/;
891 $invoice_lines += $1 || scalar(@buf);
894 die "no invoice_lines() functions in template?" unless $wasfunc;
895 my $invoice_template = new Text::Template (
897 SOURCE => [ map "$_\n", @invoice_template ],
898 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
899 $invoice_template->compile()
900 or die "can't compile template: $Text::Template::ERROR";
902 #setup template variables
903 package FS::cust_bill::_template; #!
904 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
906 $invnum = $self->invnum;
907 $date = $self->_date;
909 $agent = $self->cust_main->agent->agent;
911 if ( $FS::cust_bill::invoice_lines ) {
913 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
915 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
920 #format address (variable for the template)
922 @address = ( '', '', '', '', '', '' );
923 package FS::cust_bill; #!
924 $FS::cust_bill::_template::address[$l++] =
926 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
927 ? " (P.O. #". $cust_main->payinfo. ")"
931 $FS::cust_bill::_template::address[$l++] = $cust_main->company
932 if $cust_main->company;
933 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
934 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
935 if $cust_main->address2;
936 $FS::cust_bill::_template::address[$l++] =
937 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
938 $FS::cust_bill::_template::address[$l++] = $cust_main->country
939 unless $cust_main->country eq 'US';
941 # #overdue? (variable for the template)
942 # $FS::cust_bill::_template::overdue = (
944 # && $today > $self->_date
945 ## && $self->printed > 1
946 # && $self->printed > 0
949 #and subroutine for the template
950 sub FS::cust_bill::_template::invoice_lines {
951 my $lines = shift || scalar(@buf);
953 scalar(@buf) ? shift @buf : [ '', '' ];
959 $FS::cust_bill::_template::page = 1;
963 push @collect, split("\n",
964 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
966 $FS::cust_bill::_template::page++;
969 map "$_\n", @collect;
973 =item print_latex [ TIME [ , TEMPLATE ] ]
975 Internal method - returns a filename of a filled-in LaTeX template for this
976 invoice (Note: add ".tex" to get the actual filename).
978 See print_ps and print_pdf for methods that return PostScript and PDF output.
980 TIME an optional value used to control the printing of overdue messages. The
981 default is now. It isn't the date of the invoice; that's the `_date' field.
982 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
983 L<Time::Local> and L<Date::Parse> for conversion functions.
987 #still some false laziness w/print_text
990 my( $self, $today, $template ) = @_;
993 # my $invnum = $self->invnum;
994 my $cust_main = $self->cust_main;
995 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
996 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
998 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
999 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1000 #my $balance_due = $self->owed + $pr_total - $cr_total;
1001 my $balance_due = $self->owed + $pr_total;
1004 #my($description,$amount);
1007 #create the template
1008 $template ||= $self->_agent_template;
1009 my $templatefile = 'invoice_latex';
1010 my $suffix = length($template) ? "_$template" : '';
1011 $templatefile .= $suffix;
1012 my @invoice_template = $conf->config($templatefile)
1013 or die "cannot load config file $templatefile";
1015 my %invoice_data = (
1016 'invnum' => $self->invnum,
1017 'date' => time2str('%b %o, %Y', $self->_date),
1018 'agent' => _latex_escape($cust_main->agent->agent),
1019 'payname' => _latex_escape($cust_main->payname),
1020 'company' => _latex_escape($cust_main->company),
1021 'address1' => _latex_escape($cust_main->address1),
1022 'address2' => _latex_escape($cust_main->address2),
1023 'city' => _latex_escape($cust_main->city),
1024 'state' => _latex_escape($cust_main->state),
1025 'zip' => _latex_escape($cust_main->zip),
1026 'country' => _latex_escape($cust_main->country),
1027 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1028 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1030 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1031 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1034 my $countrydefault = $conf->config('countrydefault') || 'US';
1035 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1037 #do variable substitutions in notes
1038 $invoice_data{'notes'} =
1040 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1041 $conf->config_orbase('invoice_latexnotes', $suffix)
1044 $invoice_data{'footer'} =~ s/\n+$//;
1045 $invoice_data{'smallfooter'} =~ s/\n+$//;
1046 $invoice_data{'notes'} =~ s/\n+$//;
1048 $invoice_data{'po_line'} =
1049 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1050 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1054 my @total_item = ();
1056 while ( @invoice_template ) {
1057 my $line = shift @invoice_template;
1059 if ( $line =~ /^%%Detail\s*$/ ) {
1061 while ( ( my $line_item_line = shift @invoice_template )
1062 !~ /^%%EndDetail\s*$/ ) {
1063 push @line_item, $line_item_line;
1065 foreach my $line_item ( $self->_items ) {
1066 #foreach my $line_item ( $self->_items_pkg ) {
1067 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1068 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1069 if ( exists $line_item->{'ext_description'} ) {
1070 $invoice_data{'description'} .=
1071 "\\tabularnewline\n~~".
1072 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1074 $invoice_data{'amount'} = $line_item->{'amount'};
1075 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1077 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1080 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1082 while ( ( my $total_item_line = shift @invoice_template )
1083 !~ /^%%EndTotalDetails\s*$/ ) {
1084 push @total_item, $total_item_line;
1087 my @total_fill = ();
1090 foreach my $tax ( $self->_items_tax ) {
1091 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1092 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1094 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1099 $invoice_data{'total_item'} = 'Sub-total';
1100 $invoice_data{'total_amount'} =
1101 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1102 unshift @total_fill,
1103 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1107 $invoice_data{'total_item'} = '\textbf{Total}';
1108 $invoice_data{'total_amount'} =
1109 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1111 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1114 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1117 foreach my $credit ( $self->_items_credits ) {
1118 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1120 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1122 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1127 foreach my $payment ( $self->_items_payments ) {
1128 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1130 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1132 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1136 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1137 $invoice_data{'total_amount'} =
1138 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1140 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1143 push @filled_in, @total_fill;
1146 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1147 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1148 push @filled_in, $line;
1159 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1160 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1164 ) or die "can't open temp file: $!\n";
1165 print $fh join("\n", @filled_in ), "\n";
1168 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1173 =item print_ps [ TIME [ , TEMPLATE ] ]
1175 Returns an postscript invoice, as a scalar.
1177 TIME an optional value used to control the printing of overdue messages. The
1178 default is now. It isn't the date of the invoice; that's the `_date' field.
1179 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1180 L<Time::Local> and L<Date::Parse> for conversion functions.
1187 my $file = $self->print_latex(@_);
1189 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1192 my $sfile = shell_quote $file;
1194 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1195 or die "pslatex $file.tex failed: $!";
1196 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1197 or die "pslatex $file.tex failed: $!";
1199 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1200 or die "dvips failed: $!";
1202 open(POSTSCRIPT, "<$file.ps")
1203 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1205 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1208 while (<POSTSCRIPT>) {
1218 =item print_pdf [ TIME [ , TEMPLATE ] ]
1220 Returns an PDF invoice, as a scalar.
1222 TIME an optional value used to control the printing of overdue messages. The
1223 default is now. It isn't the date of the invoice; that's the `_date' field.
1224 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1225 L<Time::Local> and L<Date::Parse> for conversion functions.
1232 my $file = $self->print_latex(@_);
1234 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1237 #system('pdflatex', "$file.tex");
1238 #system('pdflatex', "$file.tex");
1239 #! LaTeX Error: Unknown graphics extension: .eps.
1241 my $sfile = shell_quote $file;
1243 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1244 or die "pslatex $file.tex failed: $!";
1245 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1246 or die "pslatex $file.tex failed: $!";
1248 #system('dvipdf', "$file.dvi", "$file.pdf" );
1250 "dvips -q -t letter -f $sfile.dvi ".
1251 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1254 or die "dvips | gs failed: $!";
1256 open(PDF, "<$file.pdf")
1257 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1259 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1272 # quick subroutine for print_latex
1274 # There are ten characters that LaTeX treats as special characters, which
1275 # means that they do not simply typeset themselves:
1276 # # $ % & ~ _ ^ \ { }
1278 # TeX ignores blanks following an escaped character; if you want a blank (as
1279 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1283 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1287 #utility methods for print_*
1289 sub balance_due_msg {
1291 my $msg = 'Balance Due';
1292 return $msg unless $conf->exists('invoice_default_terms');
1293 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1294 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1295 } elsif ( $conf->config('invoice_default_terms') ) {
1296 $msg .= ' - '. $conf->config('invoice_default_terms');
1303 my @display = scalar(@_)
1305 : qw( _items_previous _items_pkg );
1306 #: qw( _items_pkg );
1307 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1309 foreach my $display ( @display ) {
1310 push @b, $self->$display(@_);
1315 sub _items_previous {
1317 my $cust_main = $self->cust_main;
1318 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1320 foreach ( @pr_cust_bill ) {
1322 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1323 ' ('. time2str('%x',$_->_date). ')',
1324 #'pkgpart' => 'N/A',
1326 'amount' => sprintf("%10.2f", $_->owed),
1332 # 'description' => 'Previous Balance',
1333 # #'pkgpart' => 'N/A',
1334 # 'pkgnum' => 'N/A',
1335 # 'amount' => sprintf("%10.2f", $pr_total ),
1336 # 'ext_description' => [ map {
1337 # "Invoice ". $_->invnum.
1338 # " (". time2str("%x",$_->_date). ") ".
1339 # sprintf("%10.2f", $_->owed)
1340 # } @pr_cust_bill ],
1347 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1348 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1353 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1354 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1357 sub _items_cust_bill_pkg {
1359 my $cust_bill_pkg = shift;
1362 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1364 if ( $cust_bill_pkg->pkgnum ) {
1366 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1367 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1368 my $pkg = $part_pkg->pkg;
1370 if ( $cust_bill_pkg->setup != 0 ) {
1371 my $description = $pkg;
1372 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1373 my @d = $cust_pkg->h_labels_short($self->_date);
1374 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1376 description => $description,
1377 #pkgpart => $part_pkg->pkgpart,
1378 pkgnum => $cust_pkg->pkgnum,
1379 amount => sprintf("%10.2f", $cust_bill_pkg->setup),
1380 ext_description => \@d,
1384 if ( $cust_bill_pkg->recur != 0 ) {
1386 description => "$pkg (" .
1387 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1388 time2str('%x', $cust_bill_pkg->edate). ')',
1389 #pkgpart => $part_pkg->pkgpart,
1390 pkgnum => $cust_pkg->pkgnum,
1391 amount => sprintf("%10.2f", $cust_bill_pkg->recur),
1392 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1393 $cust_bill_pkg->sdate),
1394 $cust_bill_pkg->details,
1399 } else { #pkgnum tax or one-shot line item (??)
1401 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1402 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1404 if ( $cust_bill_pkg->setup != 0 ) {
1406 'description' => $itemdesc,
1407 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1410 if ( $cust_bill_pkg->recur != 0 ) {
1412 'description' => "$itemdesc (".
1413 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1414 time2str("%x", $cust_bill_pkg->edate). ')',
1415 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1427 sub _items_credits {
1432 foreach ( $self->cust_credited ) {
1434 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1436 my $reason = $_->cust_credit->reason;
1437 #my $reason = substr($_->cust_credit->reason,0,32);
1438 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1439 $reason = " ($reason) " if $reason;
1441 #'description' => 'Credit ref\#'. $_->crednum.
1442 # " (". time2str("%x",$_->cust_credit->_date) .")".
1444 'description' => 'Credit applied '.
1445 time2str("%x",$_->cust_credit->_date). $reason,
1446 'amount' => sprintf("%10.2f",$_->amount),
1449 #foreach ( @cr_cust_credit ) {
1451 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1452 # $money_char. sprintf("%10.2f",$_->credited)
1460 sub _items_payments {
1464 #get & print payments
1465 foreach ( $self->cust_bill_pay ) {
1467 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1470 'description' => "Payment received ".
1471 time2str("%x",$_->cust_pay->_date ),
1472 'amount' => sprintf("%10.2f", $_->amount )
1486 print_text formatting (and some logic :/) is in source, but needs to be
1487 slurped in from a file. Also number of lines ($=).
1491 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1492 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base