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
322 =item generate_email PARAMHASH
324 PARAMHASH can contain the following:
328 =item from => sender address, required
330 =item tempate => alternate template name, optional
332 =item print_text => text attachment arrayref, optional
334 =item subject => email subject, optional
338 Returns an argument list to be passed to L<FS::cust_bill::send>.
348 if ($conf->exists('invoice_email_pdf')) {
349 #warn "[FS::cust_bill::send] creating PDF attachment";
350 #mime parts arguments a la MIME::Entity->build().
353 'Type' => 'application/pdf',
354 'Encoding' => 'base64',
355 'Data' => [ $self->print_pdf('', $args{'template'}) ],
356 'Disposition' => 'attachment',
357 'Filename' => 'invoice.pdf',
363 if ($conf->exists('invoice_email_pdf')
364 and scalar($conf->config('invoice_email_pdf_note'))) {
366 #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'";
367 $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ];
369 #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'";
370 if (ref($args{'print_text'}) eq 'ARRAY') {
371 $email_text = $args{'print_text'};
373 $email_text = [ $self->print_text('', $args{'template'}) ];
378 if (ref($args{'to'} eq 'ARRAY')) {
379 @invoicing_list = @{$args{'to'}};
381 @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list;
385 'from' => $args{'from'},
386 'to' => [ @invoicing_list ],
387 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
388 'body' => $email_text,
389 'mimeparts' => $mimeparts,
395 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
397 Sends this invoice to the destinations configured for this customer: send
398 emails or print. See L<FS::cust_main_invoice>.
400 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
402 AGENTNUM, if specified, means that this invoice will only be sent for customers
403 of the specified agent.
405 INVOICE_FROM, if specified, overrides the default email invoice From: address.
411 my $template = scalar(@_) ? shift : '';
412 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
416 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
418 my @print_text = $self->print_text('', $template);
419 my @invoicing_list = $self->cust_main->invoicing_list;
421 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
423 #better to notify this person than silence
424 @invoicing_list = ($invoice_from) unless @invoicing_list;
426 my $error = send_email(
427 $self->generate_email(
428 'from' => $invoice_from,
429 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
430 'print_text' => [ @print_text ],
433 die "can't email invoice: $error\n" if $error;
434 #die "$error\n" if $error;
438 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
439 @print_text = $self->print_ps('', $template)
440 if $conf->config('invoice_latex');
441 my $lpr = $conf->config('lpr');
443 or die "Can't open pipe to $lpr: $!\n";
444 print LPR @print_text;
446 or die $! ? "Error closing $lpr: $!\n"
447 : "Exit status $? from $lpr\n";
454 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
456 Like B<send>, but only sends the invoice if it is the newest open invoice for
466 grep { $_->owed > 0 }
467 qsearch('cust_bill', {
468 'custnum' => $self->custnum,
469 #'_date' => { op=>'>', value=>$self->_date },
470 'invnum' => { op=>'>', value=>$self->invnum },
477 =item send_csv OPTIONS
479 Sends invoice as a CSV data-file to a remote host with the specified protocol.
483 protocol - currently only "ftp"
489 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
490 and YYMMDDHHMMSS is a timestamp.
492 The fields of the CSV file is as follows:
494 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
498 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
500 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
501 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
502 fields are filled in.
504 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
505 first two fields (B<record_type> and B<invnum>) and the last five fields
506 (B<pkg> through B<edate>) are filled in.
508 =item invnum - invoice number
510 =item custnum - customer number
512 =item _date - invoice date
514 =item charged - total invoice amount
516 =item first - customer first name
518 =item last - customer first name
520 =item company - company name
522 =item address1 - address line 1
524 =item address2 - address line 1
534 =item pkg - line item description
536 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
538 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
540 =item sdate - start date for recurring fee
542 =item edate - end date for recurring fee
549 my($self, %opt) = @_;
551 #part one: create file
553 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
554 mkdir $spooldir, 0700 unless -d $spooldir;
556 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
558 open(CSV, ">$file") or die "can't open $file: $!";
560 eval "use Text::CSV_XS";
563 my $csv = Text::CSV_XS->new({'always_quote'=>1});
565 my $cust_main = $self->cust_main;
571 time2str("%x", $self->_date),
572 sprintf("%.2f", $self->charged),
573 ( map { $cust_main->getfield($_) }
574 qw( first last company address1 address2 city state zip country ) ),
576 ) or die "can't create csv";
577 print CSV $csv->string. "\n";
579 #new charges (false laziness w/print_text)
580 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
582 my($pkg, $setup, $recur, $sdate, $edate);
583 if ( $cust_bill_pkg->pkgnum ) {
585 ($pkg, $setup, $recur, $sdate, $edate) = (
586 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
587 ( $cust_bill_pkg->setup != 0
588 ? sprintf("%.2f", $cust_bill_pkg->setup )
590 ( $cust_bill_pkg->recur != 0
591 ? sprintf("%.2f", $cust_bill_pkg->recur )
593 time2str("%x", $cust_bill_pkg->sdate),
594 time2str("%x", $cust_bill_pkg->edate),
598 next unless $cust_bill_pkg->setup != 0;
599 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
600 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
602 ($pkg, $setup, $recur, $sdate, $edate) =
603 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
609 ( map { '' } (1..11) ),
610 ($pkg, $setup, $recur, $sdate, $edate)
611 ) or die "can't create csv";
612 print CSV $csv->string. "\n";
616 close CSV or die "can't close CSV: $!";
621 if ( $opt{protocol} eq 'ftp' ) {
622 eval "use Net::FTP;";
624 $net = Net::FTP->new($opt{server}) or die @$;
626 die "unknown protocol: $opt{protocol}";
629 $net->login( $opt{username}, $opt{password} )
630 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
632 $net->binary or die "can't set binary mode";
634 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
636 $net->put($file) or die "can't put $file: $!";
646 Pays this invoice with a compliemntary payment. If there is an error,
647 returns the error, otherwise returns false.
653 my $cust_pay = new FS::cust_pay ( {
654 'invnum' => $self->invnum,
655 'paid' => $self->owed,
658 'payinfo' => $self->cust_main->payinfo,
666 Attempts to pay this invoice with a credit card payment via a
667 Business::OnlinePayment realtime gateway. See
668 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
669 for supported processors.
675 $self->realtime_bop( 'CC', @_ );
680 Attempts to pay this invoice with an electronic check (ACH) payment via a
681 Business::OnlinePayment realtime gateway. See
682 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
683 for supported processors.
689 $self->realtime_bop( 'ECHECK', @_ );
694 Attempts to pay this invoice with phone bill (LEC) payment via a
695 Business::OnlinePayment realtime gateway. See
696 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
697 for supported processors.
703 $self->realtime_bop( 'LEC', @_ );
707 my( $self, $method ) = @_;
709 my $cust_main = $self->cust_main;
710 my $balance = $cust_main->balance;
711 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
712 $amount = sprintf("%.2f", $amount);
713 return "not run (balance $balance)" unless $amount > 0;
715 my $description = 'Internet Services';
716 if ( $conf->exists('business-onlinepayment-description') ) {
717 my $dtempl = $conf->config('business-onlinepayment-description');
719 my $agent_obj = $cust_main->agent
720 or die "can't retreive agent for $cust_main (agentnum ".
721 $cust_main->agentnum. ")";
722 my $agent = $agent_obj->agent;
723 my $pkgs = join(', ',
724 map { $_->cust_pkg->part_pkg->pkg }
725 grep { $_->pkgnum } $self->cust_bill_pkg
727 $description = eval qq("$dtempl");
730 $cust_main->realtime_bop($method, $amount,
731 'description' => $description,
732 'invnum' => $self->invnum,
739 Adds a payment for this invoice to the pending credit card batch (see
740 L<FS::cust_pay_batch>).
746 my $cust_main = $self->cust_main;
748 my $cust_pay_batch = new FS::cust_pay_batch ( {
749 'invnum' => $self->getfield('invnum'),
750 'custnum' => $cust_main->getfield('custnum'),
751 'last' => $cust_main->getfield('last'),
752 'first' => $cust_main->getfield('first'),
753 'address1' => $cust_main->getfield('address1'),
754 'address2' => $cust_main->getfield('address2'),
755 'city' => $cust_main->getfield('city'),
756 'state' => $cust_main->getfield('state'),
757 'zip' => $cust_main->getfield('zip'),
758 'country' => $cust_main->getfield('country'),
759 'cardnum' => $cust_main->getfield('payinfo'),
760 'exp' => $cust_main->getfield('paydate'),
761 'payname' => $cust_main->getfield('payname'),
762 'amount' => $self->owed,
764 my $error = $cust_pay_batch->insert;
765 die $error if $error;
770 sub _agent_template {
772 $self->_agent_plandata('agent_templatename');
775 sub _agent_invoice_from {
777 $self->_agent_plandata('agent_invoice_from');
780 sub _agent_plandata {
781 my( $self, $option ) = @_;
783 my $part_bill_event = qsearchs( 'part_bill_event',
785 'payby' => $self->cust_main->payby,
786 'plan' => 'send_agent',
787 'plandata' => { 'op' => '~',
788 'value' => "(^|\n)agentnum ".
789 $self->cust_main->agentnum.
794 'ORDER BY seconds LIMIT 1'
797 return '' unless $part_bill_event;
799 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
802 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
803 " plandata for $option";
809 =item print_text [ TIME [ , TEMPLATE ] ]
811 Returns an text invoice, as a list of lines.
813 TIME an optional value used to control the printing of overdue messages. The
814 default is now. It isn't the date of the invoice; that's the `_date' field.
815 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
816 L<Time::Local> and L<Date::Parse> for conversion functions.
820 #still some false laziness w/print_text
823 my( $self, $today, $template ) = @_;
826 # my $invnum = $self->invnum;
827 my $cust_main = $self->cust_main;
828 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
829 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
831 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
832 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
833 #my $balance_due = $self->owed + $pr_total - $cr_total;
834 my $balance_due = $self->owed + $pr_total;
837 #my($description,$amount);
841 foreach ( @pr_cust_bill ) {
843 "Previous Balance, Invoice #". $_->invnum.
844 " (". time2str("%x",$_->_date). ")",
845 $money_char. sprintf("%10.2f",$_->owed)
849 push @buf,['','-----------'];
850 push @buf,[ 'Total Previous Balance',
851 $money_char. sprintf("%10.2f",$pr_total ) ];
856 foreach my $cust_bill_pkg (
857 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
858 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
861 if ( $cust_bill_pkg->pkgnum ) {
863 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
864 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
865 my $pkg = $part_pkg->pkg;
867 if ( $cust_bill_pkg->setup != 0 ) {
868 my $description = $pkg;
869 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
870 push @buf, [ $description,
871 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
873 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
874 $cust_pkg->h_labels($self->_date);
877 if ( $cust_bill_pkg->recur != 0 ) {
879 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
880 time2str("%x", $cust_bill_pkg->edate) . ")",
881 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
884 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
885 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
888 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
890 } else { #pkgnum tax or one-shot line item
891 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
892 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
894 if ( $cust_bill_pkg->setup != 0 ) {
895 push @buf, [ $itemdesc,
896 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
898 if ( $cust_bill_pkg->recur != 0 ) {
899 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
900 . time2str("%x", $cust_bill_pkg->edate). ")",
901 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
907 push @buf,['','-----------'];
908 push @buf,['Total New Charges',
909 $money_char. sprintf("%10.2f",$self->charged) ];
912 push @buf,['','-----------'];
913 push @buf,['Total Charges',
914 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
918 foreach ( $self->cust_credited ) {
920 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
922 my $reason = substr($_->cust_credit->reason,0,32);
923 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
924 $reason = " ($reason) " if $reason;
926 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
928 $money_char. sprintf("%10.2f",$_->amount)
931 #foreach ( @cr_cust_credit ) {
933 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
934 # $money_char. sprintf("%10.2f",$_->credited)
938 #get & print payments
939 foreach ( $self->cust_bill_pay ) {
941 #something more elaborate if $_->amount ne ->cust_pay->paid ?
944 "Payment received ". time2str("%x",$_->cust_pay->_date ),
945 $money_char. sprintf("%10.2f",$_->amount )
950 my $balance_due_msg = $self->balance_due_msg;
952 push @buf,['','-----------'];
953 push @buf,[$balance_due_msg, $money_char.
954 sprintf("%10.2f", $balance_due ) ];
957 $template ||= $self->_agent_template;
958 my $templatefile = 'invoice_template';
959 $templatefile .= "_$template" if length($template);
960 my @invoice_template = $conf->config($templatefile)
961 or die "cannot load config file $templatefile";
964 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
965 /invoice_lines\((\d*)\)/;
966 $invoice_lines += $1 || scalar(@buf);
969 die "no invoice_lines() functions in template?" unless $wasfunc;
970 my $invoice_template = new Text::Template (
972 SOURCE => [ map "$_\n", @invoice_template ],
973 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
974 $invoice_template->compile()
975 or die "can't compile template: $Text::Template::ERROR";
977 #setup template variables
978 package FS::cust_bill::_template; #!
979 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
981 $invnum = $self->invnum;
982 $date = $self->_date;
984 $agent = $self->cust_main->agent->agent;
986 if ( $FS::cust_bill::invoice_lines ) {
988 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
990 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
995 #format address (variable for the template)
997 @address = ( '', '', '', '', '', '' );
998 package FS::cust_bill; #!
999 $FS::cust_bill::_template::address[$l++] =
1000 $cust_main->payname.
1001 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1002 ? " (P.O. #". $cust_main->payinfo. ")"
1006 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1007 if $cust_main->company;
1008 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1009 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1010 if $cust_main->address2;
1011 $FS::cust_bill::_template::address[$l++] =
1012 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1013 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1014 unless $cust_main->country eq 'US';
1016 # #overdue? (variable for the template)
1017 # $FS::cust_bill::_template::overdue = (
1019 # && $today > $self->_date
1020 ## && $self->printed > 1
1021 # && $self->printed > 0
1024 #and subroutine for the template
1025 sub FS::cust_bill::_template::invoice_lines {
1026 my $lines = shift || scalar(@buf);
1028 scalar(@buf) ? shift @buf : [ '', '' ];
1034 $FS::cust_bill::_template::page = 1;
1038 push @collect, split("\n",
1039 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1041 $FS::cust_bill::_template::page++;
1044 map "$_\n", @collect;
1048 =item print_latex [ TIME [ , TEMPLATE ] ]
1050 Internal method - returns a filename of a filled-in LaTeX template for this
1051 invoice (Note: add ".tex" to get the actual filename).
1053 See print_ps and print_pdf for methods that return PostScript and PDF output.
1055 TIME an optional value used to control the printing of overdue messages. The
1056 default is now. It isn't the date of the invoice; that's the `_date' field.
1057 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1058 L<Time::Local> and L<Date::Parse> for conversion functions.
1062 #still some false laziness w/print_text
1065 my( $self, $today, $template ) = @_;
1068 # my $invnum = $self->invnum;
1069 my $cust_main = $self->cust_main;
1070 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1071 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1073 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1074 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1075 #my $balance_due = $self->owed + $pr_total - $cr_total;
1076 my $balance_due = $self->owed + $pr_total;
1079 #my($description,$amount);
1082 #create the template
1083 $template ||= $self->_agent_template;
1084 my $templatefile = 'invoice_latex';
1085 my $suffix = length($template) ? "_$template" : '';
1086 $templatefile .= $suffix;
1087 my @invoice_template = $conf->config($templatefile)
1088 or die "cannot load config file $templatefile";
1090 my %invoice_data = (
1091 'invnum' => $self->invnum,
1092 'date' => time2str('%b %o, %Y', $self->_date),
1093 'agent' => _latex_escape($cust_main->agent->agent),
1094 'payname' => _latex_escape($cust_main->payname),
1095 'company' => _latex_escape($cust_main->company),
1096 'address1' => _latex_escape($cust_main->address1),
1097 'address2' => _latex_escape($cust_main->address2),
1098 'city' => _latex_escape($cust_main->city),
1099 'state' => _latex_escape($cust_main->state),
1100 'zip' => _latex_escape($cust_main->zip),
1101 'country' => _latex_escape($cust_main->country),
1102 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1103 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1105 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1106 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1109 my $countrydefault = $conf->config('countrydefault') || 'US';
1110 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1112 #do variable substitutions in notes
1113 $invoice_data{'notes'} =
1115 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1116 $conf->config_orbase('invoice_latexnotes', $suffix)
1119 $invoice_data{'footer'} =~ s/\n+$//;
1120 $invoice_data{'smallfooter'} =~ s/\n+$//;
1121 $invoice_data{'notes'} =~ s/\n+$//;
1123 $invoice_data{'po_line'} =
1124 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1125 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1129 my @total_item = ();
1131 while ( @invoice_template ) {
1132 my $line = shift @invoice_template;
1134 if ( $line =~ /^%%Detail\s*$/ ) {
1136 while ( ( my $line_item_line = shift @invoice_template )
1137 !~ /^%%EndDetail\s*$/ ) {
1138 push @line_item, $line_item_line;
1140 foreach my $line_item ( $self->_items ) {
1141 #foreach my $line_item ( $self->_items_pkg ) {
1142 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1143 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1144 if ( exists $line_item->{'ext_description'} ) {
1145 $invoice_data{'description'} .=
1146 "\\tabularnewline\n~~".
1147 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1149 $invoice_data{'amount'} = $line_item->{'amount'};
1150 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1152 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1155 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1157 while ( ( my $total_item_line = shift @invoice_template )
1158 !~ /^%%EndTotalDetails\s*$/ ) {
1159 push @total_item, $total_item_line;
1162 my @total_fill = ();
1165 foreach my $tax ( $self->_items_tax ) {
1166 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1167 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1169 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1174 $invoice_data{'total_item'} = 'Sub-total';
1175 $invoice_data{'total_amount'} =
1176 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1177 unshift @total_fill,
1178 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1182 $invoice_data{'total_item'} = '\textbf{Total}';
1183 $invoice_data{'total_amount'} =
1184 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1186 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1189 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1192 foreach my $credit ( $self->_items_credits ) {
1193 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1195 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1197 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1202 foreach my $payment ( $self->_items_payments ) {
1203 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1205 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1207 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1211 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1212 $invoice_data{'total_amount'} =
1213 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1215 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1218 push @filled_in, @total_fill;
1221 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1222 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1223 push @filled_in, $line;
1234 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1235 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1239 ) or die "can't open temp file: $!\n";
1240 print $fh join("\n", @filled_in ), "\n";
1243 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1248 =item print_ps [ TIME [ , TEMPLATE ] ]
1250 Returns an postscript invoice, as a scalar.
1252 TIME an optional value used to control the printing of overdue messages. The
1253 default is now. It isn't the date of the invoice; that's the `_date' field.
1254 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1255 L<Time::Local> and L<Date::Parse> for conversion functions.
1262 my $file = $self->print_latex(@_);
1264 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1267 my $sfile = shell_quote $file;
1269 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1270 or die "pslatex $file.tex failed; see $file.log for details?\n";
1271 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1272 or die "pslatex $file.tex failed; see $file.log for details?\n";
1274 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1275 or die "dvips failed";
1277 open(POSTSCRIPT, "<$file.ps")
1278 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1280 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1283 while (<POSTSCRIPT>) {
1293 =item print_pdf [ TIME [ , TEMPLATE ] ]
1295 Returns an PDF invoice, as a scalar.
1297 TIME an optional value used to control the printing of overdue messages. The
1298 default is now. It isn't the date of the invoice; that's the `_date' field.
1299 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1300 L<Time::Local> and L<Date::Parse> for conversion functions.
1307 my $file = $self->print_latex(@_);
1309 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1312 #system('pdflatex', "$file.tex");
1313 #system('pdflatex', "$file.tex");
1314 #! LaTeX Error: Unknown graphics extension: .eps.
1316 my $sfile = shell_quote $file;
1318 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1319 or die "pslatex $file.tex failed: $!";
1320 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1321 or die "pslatex $file.tex failed: $!";
1323 #system('dvipdf', "$file.dvi", "$file.pdf" );
1325 "dvips -q -t letter -f $sfile.dvi ".
1326 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1329 or die "dvips | gs failed: $!";
1331 open(PDF, "<$file.pdf")
1332 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1334 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1347 # quick subroutine for print_latex
1349 # There are ten characters that LaTeX treats as special characters, which
1350 # means that they do not simply typeset themselves:
1351 # # $ % & ~ _ ^ \ { }
1353 # TeX ignores blanks following an escaped character; if you want a blank (as
1354 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1358 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1362 #utility methods for print_*
1364 sub balance_due_msg {
1366 my $msg = 'Balance Due';
1367 return $msg unless $conf->exists('invoice_default_terms');
1368 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1369 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1370 } elsif ( $conf->config('invoice_default_terms') ) {
1371 $msg .= ' - '. $conf->config('invoice_default_terms');
1378 my @display = scalar(@_)
1380 : qw( _items_previous _items_pkg );
1381 #: qw( _items_pkg );
1382 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1384 foreach my $display ( @display ) {
1385 push @b, $self->$display(@_);
1390 sub _items_previous {
1392 my $cust_main = $self->cust_main;
1393 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1395 foreach ( @pr_cust_bill ) {
1397 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1398 ' ('. time2str('%x',$_->_date). ')',
1399 #'pkgpart' => 'N/A',
1401 'amount' => sprintf("%10.2f", $_->owed),
1407 # 'description' => 'Previous Balance',
1408 # #'pkgpart' => 'N/A',
1409 # 'pkgnum' => 'N/A',
1410 # 'amount' => sprintf("%10.2f", $pr_total ),
1411 # 'ext_description' => [ map {
1412 # "Invoice ". $_->invnum.
1413 # " (". time2str("%x",$_->_date). ") ".
1414 # sprintf("%10.2f", $_->owed)
1415 # } @pr_cust_bill ],
1422 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1423 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1428 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1429 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1432 sub _items_cust_bill_pkg {
1434 my $cust_bill_pkg = shift;
1437 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1439 if ( $cust_bill_pkg->pkgnum ) {
1441 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1442 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1443 my $pkg = $part_pkg->pkg;
1445 if ( $cust_bill_pkg->setup != 0 ) {
1446 my $description = $pkg;
1447 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1448 my @d = $cust_pkg->h_labels_short($self->_date);
1449 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1451 description => $description,
1452 #pkgpart => $part_pkg->pkgpart,
1453 pkgnum => $cust_pkg->pkgnum,
1454 amount => sprintf("%10.2f", $cust_bill_pkg->setup),
1455 ext_description => \@d,
1459 if ( $cust_bill_pkg->recur != 0 ) {
1461 description => "$pkg (" .
1462 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1463 time2str('%x', $cust_bill_pkg->edate). ')',
1464 #pkgpart => $part_pkg->pkgpart,
1465 pkgnum => $cust_pkg->pkgnum,
1466 amount => sprintf("%10.2f", $cust_bill_pkg->recur),
1467 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1468 $cust_bill_pkg->sdate),
1469 $cust_bill_pkg->details,
1474 } else { #pkgnum tax or one-shot line item (??)
1476 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1477 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1479 if ( $cust_bill_pkg->setup != 0 ) {
1481 'description' => $itemdesc,
1482 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1485 if ( $cust_bill_pkg->recur != 0 ) {
1487 'description' => "$itemdesc (".
1488 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1489 time2str("%x", $cust_bill_pkg->edate). ')',
1490 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1502 sub _items_credits {
1507 foreach ( $self->cust_credited ) {
1509 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1511 my $reason = $_->cust_credit->reason;
1512 #my $reason = substr($_->cust_credit->reason,0,32);
1513 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1514 $reason = " ($reason) " if $reason;
1516 #'description' => 'Credit ref\#'. $_->crednum.
1517 # " (". time2str("%x",$_->cust_credit->_date) .")".
1519 'description' => 'Credit applied '.
1520 time2str("%x",$_->cust_credit->_date). $reason,
1521 'amount' => sprintf("%10.2f",$_->amount),
1524 #foreach ( @cr_cust_credit ) {
1526 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1527 # $money_char. sprintf("%10.2f",$_->credited)
1535 sub _items_payments {
1539 #get & print payments
1540 foreach ( $self->cust_bill_pay ) {
1542 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1545 'description' => "Payment received ".
1546 time2str("%x",$_->cust_pay->_date ),
1547 'amount' => sprintf("%10.2f", $_->amount )
1561 print_text formatting (and some logic :/) is in source, but needs to be
1562 slurped in from a file. Also number of lines ($=).
1566 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1567 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base