4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
9 use FS::UID qw( datasrc );
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
13 use FS::cust_bill_pkg;
17 use FS::cust_credit_bill;
18 use FS::cust_pay_batch;
19 use FS::cust_bill_event;
21 @ISA = qw( FS::Record );
23 #ask FS::UID to run this stuff for us later
24 FS::UID->install_callback( sub {
26 $money_char = $conf->config('money_char') || '$';
31 FS::cust_bill - Object methods for cust_bill records
37 $record = new FS::cust_bill \%hash;
38 $record = new FS::cust_bill { 'column' => 'value' };
40 $error = $record->insert;
42 $error = $new_record->replace($old_record);
44 $error = $record->delete;
46 $error = $record->check;
48 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
50 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
52 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
54 @cust_pay_objects = $cust_bill->cust_pay;
56 $tax_amount = $record->tax;
58 @lines = $cust_bill->print_text;
59 @lines = $cust_bill->print_text $time;
63 An FS::cust_bill object represents an invoice; a declaration that a customer
64 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
65 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
66 following fields are currently supported:
70 =item invnum - primary key (assigned automatically for new invoices)
72 =item custnum - customer (see L<FS::cust_main>)
74 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
75 L<Time::Local> and L<Date::Parse> for conversion functions.
77 =item charged - amount of this invoice
79 =item printed - deprecated
81 =item closed - books closed flag, empty or `Y'
91 Creates a new invoice. To add the invoice to the database, see L<"insert">.
92 Invoices are normally created by calling the bill method of a customer object
93 (see L<FS::cust_main>).
97 sub table { 'cust_bill'; }
101 Adds this invoice to the database ("Posts" the invoice). If there is an error,
102 returns the error, otherwise returns false.
106 Currently unimplemented. I don't remove invoices because there would then be
107 no record you ever posted this invoice (which is bad, no?)
113 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
114 $self->SUPER::delete(@_);
117 =item replace OLD_RECORD
119 Replaces the OLD_RECORD with this one in the database. If there is an error,
120 returns the error, otherwise returns false.
122 Only printed may be changed. printed is normally updated by calling the
123 collect method of a customer object (see L<FS::cust_main>).
128 my( $new, $old ) = ( shift, shift );
129 return "Can't change custnum!" unless $old->custnum == $new->custnum;
130 #return "Can't change _date!" unless $old->_date eq $new->_date;
131 return "Can't change _date!" unless $old->_date == $new->_date;
132 return "Can't change charged!" unless $old->charged == $new->charged;
134 $new->SUPER::replace($old);
139 Checks all fields to make sure this is a valid invoice. If there is an error,
140 returns the error, otherwise returns false. Called by the insert and replace
149 $self->ut_numbern('invnum')
150 || $self->ut_number('custnum')
151 || $self->ut_numbern('_date')
152 || $self->ut_money('charged')
153 || $self->ut_numbern('printed')
154 || $self->ut_enum('closed', [ '', 'Y' ])
156 return $error if $error;
158 return "Unknown customer"
159 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
161 $self->_date(time) unless $self->_date;
163 $self->printed(0) if $self->printed eq '';
170 Returns a list consisting of the total previous balance for this customer,
171 followed by the previous outstanding invoices (as FS::cust_bill objects also).
178 my @cust_bill = sort { $a->_date <=> $b->_date }
179 grep { $_->owed != 0 && $_->_date < $self->_date }
180 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
182 foreach ( @cust_bill ) { $total += $_->owed; }
188 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
194 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
197 =item cust_bill_event
199 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
204 sub cust_bill_event {
206 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
212 Returns the customer (see L<FS::cust_main>) for this invoice.
218 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
223 Depreciated. See the cust_credited method.
225 #Returns a list consisting of the total previous credited (see
226 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
227 #outstanding credits (FS::cust_credit objects).
233 croak "FS::cust_bill->cust_credit depreciated; see ".
234 "FS::cust_bill->cust_credit_bill";
237 #my @cust_credit = sort { $a->_date <=> $b->_date }
238 # grep { $_->credited != 0 && $_->_date < $self->_date }
239 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
241 #foreach (@cust_credit) { $total += $_->credited; }
242 #$total, @cust_credit;
247 Depreciated. See the cust_bill_pay method.
249 #Returns all payments (see L<FS::cust_pay>) for this invoice.
255 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
257 #sort { $a->_date <=> $b->_date }
258 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
264 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
270 sort { $a->_date <=> $b->_date }
271 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
276 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
282 sort { $a->_date <=> $b->_date }
283 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
289 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
296 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
298 foreach (@taxlines) { $total += $_->setup; }
304 Returns the amount owed (still outstanding) on this invoice, which is charged
305 minus all payment applications (see L<FS::cust_bill_pay>) and credit
306 applications (see L<FS::cust_credit_bill>).
312 my $balance = $self->charged;
313 $balance -= $_->amount foreach ( $self->cust_bill_pay );
314 $balance -= $_->amount foreach ( $self->cust_credited );
315 $balance = sprintf( "%.2f", $balance);
316 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
320 =item send [ TEMPLATENAME [ , AGENTNUM ] ]
322 Sends this invoice to the destinations configured for this customer: send
323 emails or print. See L<FS::cust_main_invoice>.
325 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
327 AGENTNUM, if specified, means that this invoice will only be sent for customers
328 of the specified agent.
334 my $template = scalar(@_) ? shift : '';
335 return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
337 my @print_text = $self->print_text('', $template);
338 my @invoicing_list = $self->cust_main->invoicing_list;
340 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
342 #better to notify this person than silence
343 @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
345 my $error = send_email(
346 'from' => $conf->config('invoice_from'),
347 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
348 'subject' => 'Invoice',
349 'body' => \@print_text,
351 die "can't email invoice: $error\n" if $error;
355 if ( $conf->config('invoice_latex') ) {
356 @print_text = $self->print_ps('', $template);
359 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
360 my $lpr = $conf->config('lpr');
362 or die "Can't open pipe to $lpr: $!\n";
363 print LPR @print_text;
365 or die $! ? "Error closing $lpr: $!\n"
366 : "Exit status $? from $lpr\n";
373 =item send_csv OPTIONS
375 Sends invoice as a CSV data-file to a remote host with the specified protocol.
379 protocol - currently only "ftp"
385 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
386 and YYMMDDHHMMSS is a timestamp.
388 The fields of the CSV file is as follows:
390 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
394 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
396 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
397 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
398 fields are filled in.
400 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
401 first two fields (B<record_type> and B<invnum>) and the last five fields
402 (B<pkg> through B<edate>) are filled in.
404 =item invnum - invoice number
406 =item custnum - customer number
408 =item _date - invoice date
410 =item charged - total invoice amount
412 =item first - customer first name
414 =item last - customer first name
416 =item company - company name
418 =item address1 - address line 1
420 =item address2 - address line 1
430 =item pkg - line item description
432 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
434 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
436 =item sdate - start date for recurring fee
438 =item edate - end date for recurring fee
445 my($self, %opt) = @_;
447 #part one: create file
449 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
450 mkdir $spooldir, 0700 unless -d $spooldir;
452 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
454 open(CSV, ">$file") or die "can't open $file: $!";
456 eval "use Text::CSV_XS";
459 my $csv = Text::CSV_XS->new({'always_quote'=>1});
461 my $cust_main = $self->cust_main;
467 time2str("%x", $self->_date),
468 sprintf("%.2f", $self->charged),
469 ( map { $cust_main->getfield($_) }
470 qw( first last company address1 address2 city state zip country ) ),
472 ) or die "can't create csv";
473 print CSV $csv->string. "\n";
475 #new charges (false laziness w/print_text)
476 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
478 my($pkg, $setup, $recur, $sdate, $edate);
479 if ( $cust_bill_pkg->pkgnum ) {
481 ($pkg, $setup, $recur, $sdate, $edate) = (
482 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
483 ( $cust_bill_pkg->setup != 0
484 ? sprintf("%.2f", $cust_bill_pkg->setup )
486 ( $cust_bill_pkg->recur != 0
487 ? sprintf("%.2f", $cust_bill_pkg->recur )
489 time2str("%x", $cust_bill_pkg->sdate),
490 time2str("%x", $cust_bill_pkg->edate),
494 next unless $cust_bill_pkg->setup != 0;
495 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
496 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
498 ($pkg, $setup, $recur, $sdate, $edate) =
499 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
505 ( map { '' } (1..11) ),
506 ($pkg, $setup, $recur, $sdate, $edate)
507 ) or die "can't create csv";
508 print CSV $csv->string. "\n";
512 close CSV or die "can't close CSV: $!";
517 if ( $opt{protocol} eq 'ftp' ) {
518 eval "use Net::FTP;";
520 $net = Net::FTP->new($opt{server}) or die @$;
522 die "unknown protocol: $opt{protocol}";
525 $net->login( $opt{username}, $opt{password} )
526 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
528 $net->binary or die "can't set binary mode";
530 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
532 $net->put($file) or die "can't put $file: $!";
542 Pays this invoice with a compliemntary payment. If there is an error,
543 returns the error, otherwise returns false.
549 my $cust_pay = new FS::cust_pay ( {
550 'invnum' => $self->invnum,
551 'paid' => $self->owed,
554 'payinfo' => $self->cust_main->payinfo,
562 Attempts to pay this invoice with a credit card payment via a
563 Business::OnlinePayment realtime gateway. See
564 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
565 for supported processors.
571 $self->realtime_bop( 'CC', @_ );
576 Attempts to pay this invoice with an electronic check (ACH) payment via a
577 Business::OnlinePayment realtime gateway. See
578 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
579 for supported processors.
585 $self->realtime_bop( 'ECHECK', @_ );
590 Attempts to pay this invoice with phone bill (LEC) payment via a
591 Business::OnlinePayment realtime gateway. See
592 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
593 for supported processors.
599 $self->realtime_bop( 'LEC', @_ );
603 my( $self, $method ) = @_;
605 my $cust_main = $self->cust_main;
606 my $balance = $cust_main->balance;
607 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
608 $amount = sprintf("%.2f", $amount);
609 return "not run (balance $balance)" unless $amount > 0;
611 my $description = 'Internet Services';
612 if ( $conf->exists('business-onlinepayment-description') ) {
613 my $dtempl = $conf->config('business-onlinepayment-description');
615 my $agent_obj = $cust_main->agent
616 or die "can't retreive agent for $cust_main (agentnum ".
617 $cust_main->agentnum. ")";
618 my $agent = $agent_obj->agent;
619 my $pkgs = join(', ',
620 map { $_->cust_pkg->part_pkg->pkg }
621 grep { $_->pkgnum } $self->cust_bill_pkg
623 $description = eval qq("$dtempl");
626 $cust_main->realtime_bop($method, $amount,
627 'description' => $description,
628 'invnum' => $self->invnum,
635 Adds a payment for this invoice to the pending credit card batch (see
636 L<FS::cust_pay_batch>).
642 my $cust_main = $self->cust_main;
644 my $cust_pay_batch = new FS::cust_pay_batch ( {
645 'invnum' => $self->getfield('invnum'),
646 'custnum' => $cust_main->getfield('custnum'),
647 'last' => $cust_main->getfield('last'),
648 'first' => $cust_main->getfield('first'),
649 'address1' => $cust_main->getfield('address1'),
650 'address2' => $cust_main->getfield('address2'),
651 'city' => $cust_main->getfield('city'),
652 'state' => $cust_main->getfield('state'),
653 'zip' => $cust_main->getfield('zip'),
654 'country' => $cust_main->getfield('country'),
655 'cardnum' => $cust_main->getfield('payinfo'),
656 'exp' => $cust_main->getfield('paydate'),
657 'payname' => $cust_main->getfield('payname'),
658 'amount' => $self->owed,
660 my $error = $cust_pay_batch->insert;
661 die $error if $error;
666 sub _agent_template {
669 my $cust_bill_event = qsearchs( 'part_bill_event',
671 'payby' => $self->cust_main->payby,
672 'plan' => 'send_agent',
673 'eventcode' => { 'op' => 'LIKE',
674 'value' => '_%, '. $self->cust_main->agentnum. ');' },
677 'ORDER BY seconds LIMIT 1'
680 return '' unless $cust_bill_event;
682 if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
685 warn "can't parse eventcode for agent-specific invoice template";
691 =item print_text [ TIME [ , TEMPLATE ] ]
693 Returns an text invoice, as a list of lines.
695 TIME an optional value used to control the printing of overdue messages. The
696 default is now. It isn't the date of the invoice; that's the `_date' field.
697 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
698 L<Time::Local> and L<Date::Parse> for conversion functions.
704 my( $self, $today, $template ) = @_;
706 # my $invnum = $self->invnum;
707 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
708 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
709 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
711 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
712 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
713 #my $balance_due = $self->owed + $pr_total - $cr_total;
714 my $balance_due = $self->owed + $pr_total;
717 #my($description,$amount);
721 foreach ( @pr_cust_bill ) {
723 "Previous Balance, Invoice #". $_->invnum.
724 " (". time2str("%x",$_->_date). ")",
725 $money_char. sprintf("%10.2f",$_->owed)
729 push @buf,['','-----------'];
730 push @buf,[ 'Total Previous Balance',
731 $money_char. sprintf("%10.2f",$pr_total ) ];
736 foreach my $cust_bill_pkg (
737 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
738 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
741 if ( $cust_bill_pkg->pkgnum ) {
743 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
744 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
745 my $pkg = $part_pkg->pkg;
747 if ( $cust_bill_pkg->setup != 0 ) {
748 my $description = $pkg;
749 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
750 push @buf, [ $description,
751 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
753 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
756 if ( $cust_bill_pkg->recur != 0 ) {
758 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
759 time2str("%x", $cust_bill_pkg->edate) . ")",
760 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
763 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
766 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
768 } else { #pkgnum tax or one-shot line item
769 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
770 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
772 if ( $cust_bill_pkg->setup != 0 ) {
773 push @buf, [ $itemdesc,
774 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
776 if ( $cust_bill_pkg->recur != 0 ) {
777 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
778 . time2str("%x", $cust_bill_pkg->edate). ")",
779 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
785 push @buf,['','-----------'];
786 push @buf,['Total New Charges',
787 $money_char. sprintf("%10.2f",$self->charged) ];
790 push @buf,['','-----------'];
791 push @buf,['Total Charges',
792 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
796 foreach ( $self->cust_credited ) {
798 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
800 my $reason = substr($_->cust_credit->reason,0,32);
801 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
802 $reason = " ($reason) " if $reason;
804 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
806 $money_char. sprintf("%10.2f",$_->amount)
809 #foreach ( @cr_cust_credit ) {
811 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
812 # $money_char. sprintf("%10.2f",$_->credited)
816 #get & print payments
817 foreach ( $self->cust_bill_pay ) {
819 #something more elaborate if $_->amount ne ->cust_pay->paid ?
822 "Payment received ". time2str("%x",$_->cust_pay->_date ),
823 $money_char. sprintf("%10.2f",$_->amount )
828 my $balance_due_msg = $self->balance_due_msg;
830 push @buf,['','-----------'];
831 push @buf,[$balance_due_msg, $money_char.
832 sprintf("%10.2f", $balance_due ) ];
835 $template ||= $self->_agent_template;
836 my $templatefile = 'invoice_template';
837 $templatefile .= "_$template" if length($template);
838 my @invoice_template = $conf->config($templatefile)
839 or die "cannot load config file $templatefile";
842 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
843 /invoice_lines\((\d*)\)/;
844 $invoice_lines += $1 || scalar(@buf);
847 die "no invoice_lines() functions in template?" unless $wasfunc;
848 my $invoice_template = new Text::Template (
850 SOURCE => [ map "$_\n", @invoice_template ],
851 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
852 $invoice_template->compile()
853 or die "can't compile template: $Text::Template::ERROR";
855 #setup template variables
856 package FS::cust_bill::_template; #!
857 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
859 $invnum = $self->invnum;
860 $date = $self->_date;
862 $agent = $self->cust_main->agent->agent;
864 if ( $FS::cust_bill::invoice_lines ) {
866 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
868 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
873 #format address (variable for the template)
875 @address = ( '', '', '', '', '', '' );
876 package FS::cust_bill; #!
877 $FS::cust_bill::_template::address[$l++] =
879 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
880 ? " (P.O. #". $cust_main->payinfo. ")"
884 $FS::cust_bill::_template::address[$l++] = $cust_main->company
885 if $cust_main->company;
886 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
887 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
888 if $cust_main->address2;
889 $FS::cust_bill::_template::address[$l++] =
890 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
891 $FS::cust_bill::_template::address[$l++] = $cust_main->country
892 unless $cust_main->country eq 'US';
894 # #overdue? (variable for the template)
895 # $FS::cust_bill::_template::overdue = (
897 # && $today > $self->_date
898 ## && $self->printed > 1
899 # && $self->printed > 0
902 #and subroutine for the template
903 sub FS::cust_bill::_template::invoice_lines {
904 my $lines = shift || scalar(@buf);
906 scalar(@buf) ? shift @buf : [ '', '' ];
912 $FS::cust_bill::_template::page = 1;
916 push @collect, split("\n",
917 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
919 $FS::cust_bill::_template::page++;
922 map "$_\n", @collect;
926 =item print_latex [ TIME [ , TEMPLATE ] ]
928 Internal method - returns a filename of a filled-in LaTeX template for this
929 invoice (Note: add ".tex" to get the actual filename).
931 See print_ps and print_pdf for methods that return PostScript and PDF output.
933 TIME an optional value used to control the printing of overdue messages. The
934 default is now. It isn't the date of the invoice; that's the `_date' field.
935 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
936 L<Time::Local> and L<Date::Parse> for conversion functions.
940 #still some false laziness w/print_text
943 my( $self, $today, $template ) = @_;
946 # my $invnum = $self->invnum;
947 my $cust_main = $self->cust_main;
948 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
949 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
951 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
952 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
953 #my $balance_due = $self->owed + $pr_total - $cr_total;
954 my $balance_due = $self->owed + $pr_total;
957 #my($description,$amount);
961 $template ||= $self->_agent_template;
962 my $templatefile = 'invoice_latex';
963 my $suffix = length($template) ? "_$template" : '';
964 $templatefile .= $suffix;
965 my @invoice_template = $conf->config($templatefile)
966 or die "cannot load config file $templatefile";
969 'invnum' => $self->invnum,
970 'date' => time2str('%b %o, %Y', $self->_date),
971 'agent' => _latex_escape($cust_main->agent->agent),
972 'payname' => _latex_escape($cust_main->payname),
973 'company' => _latex_escape($cust_main->company),
974 'address1' => _latex_escape($cust_main->address1),
975 'address2' => _latex_escape($cust_main->address2),
976 'city' => _latex_escape($cust_main->city),
977 'state' => _latex_escape($cust_main->state),
978 'zip' => _latex_escape($cust_main->zip),
979 'country' => _latex_escape($cust_main->country),
980 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
981 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
983 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
984 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
987 my $countrydefault = $conf->config('countrydefault') || 'US';
988 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
990 #do variable substitutions in notes
991 $invoice_data{'notes'} =
993 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
994 $conf->config_orbase('invoice_latexnotes', $suffix)
997 $invoice_data{'footer'} =~ s/\n+$//;
998 $invoice_data{'smallfooter'} =~ s/\n+$//;
999 $invoice_data{'notes'} =~ s/\n+$//;
1001 $invoice_data{'po_line'} =
1002 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1003 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1007 my @total_item = ();
1009 while ( @invoice_template ) {
1010 my $line = shift @invoice_template;
1012 if ( $line =~ /^%%Detail\s*$/ ) {
1014 while ( ( my $line_item_line = shift @invoice_template )
1015 !~ /^%%EndDetail\s*$/ ) {
1016 push @line_item, $line_item_line;
1018 foreach my $line_item ( $self->_items ) {
1019 #foreach my $line_item ( $self->_items_pkg ) {
1020 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1021 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1022 if ( exists $line_item->{'ext_description'} ) {
1023 $invoice_data{'description'} .=
1024 "\\tabularnewline\n~~".
1025 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1027 $invoice_data{'amount'} = $line_item->{'amount'};
1028 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1030 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1033 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1035 while ( ( my $total_item_line = shift @invoice_template )
1036 !~ /^%%EndTotalDetails\s*$/ ) {
1037 push @total_item, $total_item_line;
1040 my @total_fill = ();
1043 foreach my $tax ( $self->_items_tax ) {
1044 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1045 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1047 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1052 $invoice_data{'total_item'} = 'Sub-total';
1053 $invoice_data{'total_amount'} =
1054 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1055 unshift @total_fill,
1056 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1060 $invoice_data{'total_item'} = '\textbf{Total}';
1061 $invoice_data{'total_amount'} =
1062 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1064 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1067 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1070 foreach my $credit ( $self->_items_credits ) {
1071 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1073 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1075 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1080 foreach my $payment ( $self->_items_payments ) {
1081 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1083 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1085 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1089 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1090 $invoice_data{'total_amount'} =
1091 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1093 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1096 push @filled_in, @total_fill;
1099 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1100 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1101 push @filled_in, $line;
1112 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1113 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1117 ) or die "can't open temp file: $!\n";
1118 print $fh join("\n", @filled_in ), "\n";
1121 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1126 =item print_ps [ TIME [ , TEMPLATE ] ]
1128 Returns an postscript invoice, as a scalar.
1130 TIME an optional value used to control the printing of overdue messages. The
1131 default is now. It isn't the date of the invoice; that's the `_date' field.
1132 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1133 L<Time::Local> and L<Date::Parse> for conversion functions.
1140 my $file = $self->print_latex(@_);
1142 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1145 system("pslatex $file.tex >/dev/null 2>&1") == 0
1146 or die "pslatex $file.tex failed: $!";
1147 system("pslatex $file.tex >/dev/null 2>&1") == 0
1148 or die "pslatex $file.tex failed: $!";
1150 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1151 or die "dvips failed: $!";
1153 open(POSTSCRIPT, "<$file.ps")
1154 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1156 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1159 while (<POSTSCRIPT>) {
1169 =item print_pdf [ TIME [ , TEMPLATE ] ]
1171 Returns an PDF invoice, as a scalar.
1173 TIME an optional value used to control the printing of overdue messages. The
1174 default is now. It isn't the date of the invoice; that's the `_date' field.
1175 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1176 L<Time::Local> and L<Date::Parse> for conversion functions.
1183 my $file = $self->print_latex(@_);
1185 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1188 #system('pdflatex', "$file.tex");
1189 #system('pdflatex', "$file.tex");
1190 #! LaTeX Error: Unknown graphics extension: .eps.
1192 system("pslatex $file.tex >/dev/null 2>&1") == 0
1193 or die "pslatex $file.tex failed: $!";
1194 system("pslatex $file.tex >/dev/null 2>&1") == 0
1195 or die "pslatex $file.tex failed: $!";
1197 #system('dvipdf', "$file.dvi", "$file.pdf" );
1199 "dvips -q -t letter -f $file.dvi ".
1200 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
1203 or die "dvips | gs failed: $!";
1205 open(PDF, "<$file.pdf")
1206 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1208 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1221 # quick subroutine for print_latex
1223 # There are ten characters that LaTeX treats as special characters, which
1224 # means that they do not simply typeset themselves:
1225 # # $ % & ~ _ ^ \ { }
1227 # TeX ignores blanks following an escaped character; if you want a blank (as
1228 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1232 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1236 #utility methods for print_*
1238 sub balance_due_msg {
1240 my $msg = 'Balance Due';
1241 return $msg unless $conf->exists('invoice_default_terms');
1242 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1243 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1244 } elsif ( $conf->config('invoice_default_terms') ) {
1245 $msg .= ' - '. $conf->config('invoice_default_terms');
1252 my @display = scalar(@_)
1254 : qw( _items_previous _items_pkg );
1255 #: qw( _items_pkg );
1256 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1258 foreach my $display ( @display ) {
1259 push @b, $self->$display(@_);
1264 sub _items_previous {
1266 my $cust_main = $self->cust_main;
1267 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1269 foreach ( @pr_cust_bill ) {
1271 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1272 ' ('. time2str('%x',$_->_date). ')',
1273 #'pkgpart' => 'N/A',
1275 'amount' => sprintf("%10.2f", $_->owed),
1281 # 'description' => 'Previous Balance',
1282 # #'pkgpart' => 'N/A',
1283 # 'pkgnum' => 'N/A',
1284 # 'amount' => sprintf("%10.2f", $pr_total ),
1285 # 'ext_description' => [ map {
1286 # "Invoice ". $_->invnum.
1287 # " (". time2str("%x",$_->_date). ") ".
1288 # sprintf("%10.2f", $_->owed)
1289 # } @pr_cust_bill ],
1296 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1297 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1302 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1303 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1306 sub _items_cust_bill_pkg {
1308 my $cust_bill_pkg = shift;
1311 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1313 if ( $cust_bill_pkg->pkgnum ) {
1315 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1316 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1317 my $pkg = $part_pkg->pkg;
1320 #tie %labels, 'Tie::IxHash';
1321 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1322 my @ext_description;
1323 foreach my $label ( keys %labels ) {
1324 my @values = @{ $labels{$label} };
1325 my $num = scalar(@values);
1327 push @ext_description, "$label ($num)";
1329 push @ext_description, map { "$label: $_" } @values;
1333 if ( $cust_bill_pkg->setup != 0 ) {
1334 my $description = $pkg;
1335 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1336 my @d = @ext_description;
1337 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1339 'description' => $description,
1340 #'pkgpart' => $part_pkg->pkgpart,
1341 'pkgnum' => $cust_pkg->pkgnum,
1342 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1343 'ext_description' => \@d,
1347 if ( $cust_bill_pkg->recur != 0 ) {
1349 'description' => "$pkg (" .
1350 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1351 time2str('%x', $cust_bill_pkg->edate). ')',
1352 #'pkgpart' => $part_pkg->pkgpart,
1353 'pkgnum' => $cust_pkg->pkgnum,
1354 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1355 'ext_description' => [ @ext_description,
1356 $cust_bill_pkg->details,
1361 } else { #pkgnum tax or one-shot line item (??)
1363 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1364 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1366 if ( $cust_bill_pkg->setup != 0 ) {
1368 'description' => $itemdesc,
1369 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1372 if ( $cust_bill_pkg->recur != 0 ) {
1374 'description' => "$itemdesc (".
1375 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1376 time2str("%x", $cust_bill_pkg->edate). ')',
1377 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1389 sub _items_credits {
1394 foreach ( $self->cust_credited ) {
1396 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1398 my $reason = $_->cust_credit->reason;
1399 #my $reason = substr($_->cust_credit->reason,0,32);
1400 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1401 $reason = " ($reason) " if $reason;
1403 #'description' => 'Credit ref\#'. $_->crednum.
1404 # " (". time2str("%x",$_->cust_credit->_date) .")".
1406 'description' => 'Credit applied'.
1407 time2str("%x",$_->cust_credit->_date). $reason,
1408 'amount' => sprintf("%10.2f",$_->amount),
1411 #foreach ( @cr_cust_credit ) {
1413 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1414 # $money_char. sprintf("%10.2f",$_->credited)
1422 sub _items_payments {
1426 #get & print payments
1427 foreach ( $self->cust_bill_pay ) {
1429 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1432 'description' => "Payment received ".
1433 time2str("%x",$_->cust_pay->_date ),
1434 'amount' => sprintf("%10.2f", $_->amount )
1448 print_text formatting (and some logic :/) is in source, but needs to be
1449 slurped in from a file. Also number of lines ($=).
1453 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1454 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base