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 [ , INVOICE_FROM ] ] ]
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.
330 INVOICE_FROM, if specified, overrides the default email invoice From: address.
336 my $template = scalar(@_) ? shift : '';
337 return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
338 my $invoice_from = scalar(@_) ? shift : $conf->config('invoice_from');
340 my @print_text = $self->print_text('', $template);
341 my @invoicing_list = $self->cust_main->invoicing_list;
343 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
345 #better to notify this person than silence
346 @invoicing_list = ($invoice_from) unless @invoicing_list;
348 my $error = send_email(
349 'from' => $invoice_from,
350 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
351 'subject' => 'Invoice',
352 'body' => \@print_text,
354 die "can't email invoice: $error\n" if $error;
358 if ( $conf->config('invoice_latex') ) {
359 @print_text = $self->print_ps('', $template);
362 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
363 my $lpr = $conf->config('lpr');
365 or die "Can't open pipe to $lpr: $!\n";
366 print LPR @print_text;
368 or die $! ? "Error closing $lpr: $!\n"
369 : "Exit status $? from $lpr\n";
376 =item send_csv OPTIONS
378 Sends invoice as a CSV data-file to a remote host with the specified protocol.
382 protocol - currently only "ftp"
388 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
389 and YYMMDDHHMMSS is a timestamp.
391 The fields of the CSV file is as follows:
393 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
397 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
399 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
400 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
401 fields are filled in.
403 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
404 first two fields (B<record_type> and B<invnum>) and the last five fields
405 (B<pkg> through B<edate>) are filled in.
407 =item invnum - invoice number
409 =item custnum - customer number
411 =item _date - invoice date
413 =item charged - total invoice amount
415 =item first - customer first name
417 =item last - customer first name
419 =item company - company name
421 =item address1 - address line 1
423 =item address2 - address line 1
433 =item pkg - line item description
435 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
437 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
439 =item sdate - start date for recurring fee
441 =item edate - end date for recurring fee
448 my($self, %opt) = @_;
450 #part one: create file
452 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
453 mkdir $spooldir, 0700 unless -d $spooldir;
455 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
457 open(CSV, ">$file") or die "can't open $file: $!";
459 eval "use Text::CSV_XS";
462 my $csv = Text::CSV_XS->new({'always_quote'=>1});
464 my $cust_main = $self->cust_main;
470 time2str("%x", $self->_date),
471 sprintf("%.2f", $self->charged),
472 ( map { $cust_main->getfield($_) }
473 qw( first last company address1 address2 city state zip country ) ),
475 ) or die "can't create csv";
476 print CSV $csv->string. "\n";
478 #new charges (false laziness w/print_text)
479 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
481 my($pkg, $setup, $recur, $sdate, $edate);
482 if ( $cust_bill_pkg->pkgnum ) {
484 ($pkg, $setup, $recur, $sdate, $edate) = (
485 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
486 ( $cust_bill_pkg->setup != 0
487 ? sprintf("%.2f", $cust_bill_pkg->setup )
489 ( $cust_bill_pkg->recur != 0
490 ? sprintf("%.2f", $cust_bill_pkg->recur )
492 time2str("%x", $cust_bill_pkg->sdate),
493 time2str("%x", $cust_bill_pkg->edate),
497 next unless $cust_bill_pkg->setup != 0;
498 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
499 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
501 ($pkg, $setup, $recur, $sdate, $edate) =
502 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
508 ( map { '' } (1..11) ),
509 ($pkg, $setup, $recur, $sdate, $edate)
510 ) or die "can't create csv";
511 print CSV $csv->string. "\n";
515 close CSV or die "can't close CSV: $!";
520 if ( $opt{protocol} eq 'ftp' ) {
521 eval "use Net::FTP;";
523 $net = Net::FTP->new($opt{server}) or die @$;
525 die "unknown protocol: $opt{protocol}";
528 $net->login( $opt{username}, $opt{password} )
529 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
531 $net->binary or die "can't set binary mode";
533 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
535 $net->put($file) or die "can't put $file: $!";
545 Pays this invoice with a compliemntary payment. If there is an error,
546 returns the error, otherwise returns false.
552 my $cust_pay = new FS::cust_pay ( {
553 'invnum' => $self->invnum,
554 'paid' => $self->owed,
557 'payinfo' => $self->cust_main->payinfo,
565 Attempts to pay this invoice with a credit card payment via a
566 Business::OnlinePayment realtime gateway. See
567 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
568 for supported processors.
574 $self->realtime_bop( 'CC', @_ );
579 Attempts to pay this invoice with an electronic check (ACH) payment via a
580 Business::OnlinePayment realtime gateway. See
581 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
582 for supported processors.
588 $self->realtime_bop( 'ECHECK', @_ );
593 Attempts to pay this invoice with phone bill (LEC) payment via a
594 Business::OnlinePayment realtime gateway. See
595 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
596 for supported processors.
602 $self->realtime_bop( 'LEC', @_ );
606 my( $self, $method ) = @_;
608 my $cust_main = $self->cust_main;
609 my $balance = $cust_main->balance;
610 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
611 $amount = sprintf("%.2f", $amount);
612 return "not run (balance $balance)" unless $amount > 0;
614 my $description = 'Internet Services';
615 if ( $conf->exists('business-onlinepayment-description') ) {
616 my $dtempl = $conf->config('business-onlinepayment-description');
618 my $agent_obj = $cust_main->agent
619 or die "can't retreive agent for $cust_main (agentnum ".
620 $cust_main->agentnum. ")";
621 my $agent = $agent_obj->agent;
622 my $pkgs = join(', ',
623 map { $_->cust_pkg->part_pkg->pkg }
624 grep { $_->pkgnum } $self->cust_bill_pkg
626 $description = eval qq("$dtempl");
629 $cust_main->realtime_bop($method, $amount,
630 'description' => $description,
631 'invnum' => $self->invnum,
638 Adds a payment for this invoice to the pending credit card batch (see
639 L<FS::cust_pay_batch>).
645 my $cust_main = $self->cust_main;
647 my $cust_pay_batch = new FS::cust_pay_batch ( {
648 'invnum' => $self->getfield('invnum'),
649 'custnum' => $cust_main->getfield('custnum'),
650 'last' => $cust_main->getfield('last'),
651 'first' => $cust_main->getfield('first'),
652 'address1' => $cust_main->getfield('address1'),
653 'address2' => $cust_main->getfield('address2'),
654 'city' => $cust_main->getfield('city'),
655 'state' => $cust_main->getfield('state'),
656 'zip' => $cust_main->getfield('zip'),
657 'country' => $cust_main->getfield('country'),
658 'cardnum' => $cust_main->getfield('payinfo'),
659 'exp' => $cust_main->getfield('paydate'),
660 'payname' => $cust_main->getfield('payname'),
661 'amount' => $self->owed,
663 my $error = $cust_pay_batch->insert;
664 die $error if $error;
669 sub _agent_template {
672 my $cust_bill_event = qsearchs( 'part_bill_event',
674 'payby' => $self->cust_main->payby,
675 'plan' => 'send_agent',
676 'eventcode' => { 'op' => 'LIKE',
677 'value' => '_%, '. $self->cust_main->agentnum. ');' },
680 'ORDER BY seconds LIMIT 1'
683 return '' unless $cust_bill_event;
685 if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
688 warn "can't parse eventcode for agent-specific invoice template";
694 =item print_text [ TIME [ , TEMPLATE ] ]
696 Returns an text invoice, as a list of lines.
698 TIME an optional value used to control the printing of overdue messages. The
699 default is now. It isn't the date of the invoice; that's the `_date' field.
700 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
701 L<Time::Local> and L<Date::Parse> for conversion functions.
707 my( $self, $today, $template ) = @_;
709 # my $invnum = $self->invnum;
710 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
711 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
712 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
714 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
715 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
716 #my $balance_due = $self->owed + $pr_total - $cr_total;
717 my $balance_due = $self->owed + $pr_total;
720 #my($description,$amount);
724 foreach ( @pr_cust_bill ) {
726 "Previous Balance, Invoice #". $_->invnum.
727 " (". time2str("%x",$_->_date). ")",
728 $money_char. sprintf("%10.2f",$_->owed)
732 push @buf,['','-----------'];
733 push @buf,[ 'Total Previous Balance',
734 $money_char. sprintf("%10.2f",$pr_total ) ];
739 foreach my $cust_bill_pkg (
740 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
741 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
744 if ( $cust_bill_pkg->pkgnum ) {
746 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
747 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
748 my $pkg = $part_pkg->pkg;
750 if ( $cust_bill_pkg->setup != 0 ) {
751 my $description = $pkg;
752 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
753 push @buf, [ $description,
754 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
756 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
759 if ( $cust_bill_pkg->recur != 0 ) {
761 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
762 time2str("%x", $cust_bill_pkg->edate) . ")",
763 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
766 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
769 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
771 } else { #pkgnum tax or one-shot line item
772 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
773 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
775 if ( $cust_bill_pkg->setup != 0 ) {
776 push @buf, [ $itemdesc,
777 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
779 if ( $cust_bill_pkg->recur != 0 ) {
780 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
781 . time2str("%x", $cust_bill_pkg->edate). ")",
782 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
788 push @buf,['','-----------'];
789 push @buf,['Total New Charges',
790 $money_char. sprintf("%10.2f",$self->charged) ];
793 push @buf,['','-----------'];
794 push @buf,['Total Charges',
795 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
799 foreach ( $self->cust_credited ) {
801 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
803 my $reason = substr($_->cust_credit->reason,0,32);
804 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
805 $reason = " ($reason) " if $reason;
807 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
809 $money_char. sprintf("%10.2f",$_->amount)
812 #foreach ( @cr_cust_credit ) {
814 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
815 # $money_char. sprintf("%10.2f",$_->credited)
819 #get & print payments
820 foreach ( $self->cust_bill_pay ) {
822 #something more elaborate if $_->amount ne ->cust_pay->paid ?
825 "Payment received ". time2str("%x",$_->cust_pay->_date ),
826 $money_char. sprintf("%10.2f",$_->amount )
831 my $balance_due_msg = $self->balance_due_msg;
833 push @buf,['','-----------'];
834 push @buf,[$balance_due_msg, $money_char.
835 sprintf("%10.2f", $balance_due ) ];
838 $template ||= $self->_agent_template;
839 my $templatefile = 'invoice_template';
840 $templatefile .= "_$template" if length($template);
841 my @invoice_template = $conf->config($templatefile)
842 or die "cannot load config file $templatefile";
845 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
846 /invoice_lines\((\d*)\)/;
847 $invoice_lines += $1 || scalar(@buf);
850 die "no invoice_lines() functions in template?" unless $wasfunc;
851 my $invoice_template = new Text::Template (
853 SOURCE => [ map "$_\n", @invoice_template ],
854 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
855 $invoice_template->compile()
856 or die "can't compile template: $Text::Template::ERROR";
858 #setup template variables
859 package FS::cust_bill::_template; #!
860 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
862 $invnum = $self->invnum;
863 $date = $self->_date;
865 $agent = $self->cust_main->agent->agent;
867 if ( $FS::cust_bill::invoice_lines ) {
869 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
871 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
876 #format address (variable for the template)
878 @address = ( '', '', '', '', '', '' );
879 package FS::cust_bill; #!
880 $FS::cust_bill::_template::address[$l++] =
882 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
883 ? " (P.O. #". $cust_main->payinfo. ")"
887 $FS::cust_bill::_template::address[$l++] = $cust_main->company
888 if $cust_main->company;
889 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
890 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
891 if $cust_main->address2;
892 $FS::cust_bill::_template::address[$l++] =
893 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
894 $FS::cust_bill::_template::address[$l++] = $cust_main->country
895 unless $cust_main->country eq 'US';
897 # #overdue? (variable for the template)
898 # $FS::cust_bill::_template::overdue = (
900 # && $today > $self->_date
901 ## && $self->printed > 1
902 # && $self->printed > 0
905 #and subroutine for the template
906 sub FS::cust_bill::_template::invoice_lines {
907 my $lines = shift || scalar(@buf);
909 scalar(@buf) ? shift @buf : [ '', '' ];
915 $FS::cust_bill::_template::page = 1;
919 push @collect, split("\n",
920 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
922 $FS::cust_bill::_template::page++;
925 map "$_\n", @collect;
929 =item print_latex [ TIME [ , TEMPLATE ] ]
931 Internal method - returns a filename of a filled-in LaTeX template for this
932 invoice (Note: add ".tex" to get the actual filename).
934 See print_ps and print_pdf for methods that return PostScript and PDF output.
936 TIME an optional value used to control the printing of overdue messages. The
937 default is now. It isn't the date of the invoice; that's the `_date' field.
938 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
939 L<Time::Local> and L<Date::Parse> for conversion functions.
943 #still some false laziness w/print_text
946 my( $self, $today, $template ) = @_;
949 # my $invnum = $self->invnum;
950 my $cust_main = $self->cust_main;
951 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
952 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
954 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
955 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
956 #my $balance_due = $self->owed + $pr_total - $cr_total;
957 my $balance_due = $self->owed + $pr_total;
960 #my($description,$amount);
964 $template ||= $self->_agent_template;
965 my $templatefile = 'invoice_latex';
966 my $suffix = length($template) ? "_$template" : '';
967 $templatefile .= $suffix;
968 my @invoice_template = $conf->config($templatefile)
969 or die "cannot load config file $templatefile";
972 'invnum' => $self->invnum,
973 'date' => time2str('%b %o, %Y', $self->_date),
974 'agent' => _latex_escape($cust_main->agent->agent),
975 'payname' => _latex_escape($cust_main->payname),
976 'company' => _latex_escape($cust_main->company),
977 'address1' => _latex_escape($cust_main->address1),
978 'address2' => _latex_escape($cust_main->address2),
979 'city' => _latex_escape($cust_main->city),
980 'state' => _latex_escape($cust_main->state),
981 'zip' => _latex_escape($cust_main->zip),
982 'country' => _latex_escape($cust_main->country),
983 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
984 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
986 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
987 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
990 my $countrydefault = $conf->config('countrydefault') || 'US';
991 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
993 #do variable substitutions in notes
994 $invoice_data{'notes'} =
996 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
997 $conf->config_orbase('invoice_latexnotes', $suffix)
1000 $invoice_data{'footer'} =~ s/\n+$//;
1001 $invoice_data{'smallfooter'} =~ s/\n+$//;
1002 $invoice_data{'notes'} =~ s/\n+$//;
1004 $invoice_data{'po_line'} =
1005 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1006 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1010 my @total_item = ();
1012 while ( @invoice_template ) {
1013 my $line = shift @invoice_template;
1015 if ( $line =~ /^%%Detail\s*$/ ) {
1017 while ( ( my $line_item_line = shift @invoice_template )
1018 !~ /^%%EndDetail\s*$/ ) {
1019 push @line_item, $line_item_line;
1021 foreach my $line_item ( $self->_items ) {
1022 #foreach my $line_item ( $self->_items_pkg ) {
1023 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1024 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1025 if ( exists $line_item->{'ext_description'} ) {
1026 $invoice_data{'description'} .=
1027 "\\tabularnewline\n~~".
1028 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1030 $invoice_data{'amount'} = $line_item->{'amount'};
1031 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1033 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1036 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1038 while ( ( my $total_item_line = shift @invoice_template )
1039 !~ /^%%EndTotalDetails\s*$/ ) {
1040 push @total_item, $total_item_line;
1043 my @total_fill = ();
1046 foreach my $tax ( $self->_items_tax ) {
1047 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1048 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1050 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1055 $invoice_data{'total_item'} = 'Sub-total';
1056 $invoice_data{'total_amount'} =
1057 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1058 unshift @total_fill,
1059 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1063 $invoice_data{'total_item'} = '\textbf{Total}';
1064 $invoice_data{'total_amount'} =
1065 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1067 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1070 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1073 foreach my $credit ( $self->_items_credits ) {
1074 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1076 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1078 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1083 foreach my $payment ( $self->_items_payments ) {
1084 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1086 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1088 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1092 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1093 $invoice_data{'total_amount'} =
1094 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1096 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1099 push @filled_in, @total_fill;
1102 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1103 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1104 push @filled_in, $line;
1115 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1116 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1120 ) or die "can't open temp file: $!\n";
1121 print $fh join("\n", @filled_in ), "\n";
1124 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1129 =item print_ps [ TIME [ , TEMPLATE ] ]
1131 Returns an postscript invoice, as a scalar.
1133 TIME an optional value used to control the printing of overdue messages. The
1134 default is now. It isn't the date of the invoice; that's the `_date' field.
1135 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1136 L<Time::Local> and L<Date::Parse> for conversion functions.
1143 my $file = $self->print_latex(@_);
1145 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1148 system("pslatex $file.tex >/dev/null 2>&1") == 0
1149 or die "pslatex $file.tex failed: $!";
1150 system("pslatex $file.tex >/dev/null 2>&1") == 0
1151 or die "pslatex $file.tex failed: $!";
1153 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1154 or die "dvips failed: $!";
1156 open(POSTSCRIPT, "<$file.ps")
1157 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1159 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1162 while (<POSTSCRIPT>) {
1172 =item print_pdf [ TIME [ , TEMPLATE ] ]
1174 Returns an PDF invoice, as a scalar.
1176 TIME an optional value used to control the printing of overdue messages. The
1177 default is now. It isn't the date of the invoice; that's the `_date' field.
1178 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1179 L<Time::Local> and L<Date::Parse> for conversion functions.
1186 my $file = $self->print_latex(@_);
1188 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1191 #system('pdflatex', "$file.tex");
1192 #system('pdflatex', "$file.tex");
1193 #! LaTeX Error: Unknown graphics extension: .eps.
1195 system("pslatex $file.tex >/dev/null 2>&1") == 0
1196 or die "pslatex $file.tex failed: $!";
1197 system("pslatex $file.tex >/dev/null 2>&1") == 0
1198 or die "pslatex $file.tex failed: $!";
1200 #system('dvipdf', "$file.dvi", "$file.pdf" );
1202 "dvips -q -t letter -f $file.dvi ".
1203 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
1206 or die "dvips | gs failed: $!";
1208 open(PDF, "<$file.pdf")
1209 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1211 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1224 # quick subroutine for print_latex
1226 # There are ten characters that LaTeX treats as special characters, which
1227 # means that they do not simply typeset themselves:
1228 # # $ % & ~ _ ^ \ { }
1230 # TeX ignores blanks following an escaped character; if you want a blank (as
1231 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1235 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1239 #utility methods for print_*
1241 sub balance_due_msg {
1243 my $msg = 'Balance Due';
1244 return $msg unless $conf->exists('invoice_default_terms');
1245 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1246 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1247 } elsif ( $conf->config('invoice_default_terms') ) {
1248 $msg .= ' - '. $conf->config('invoice_default_terms');
1255 my @display = scalar(@_)
1257 : qw( _items_previous _items_pkg );
1258 #: qw( _items_pkg );
1259 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1261 foreach my $display ( @display ) {
1262 push @b, $self->$display(@_);
1267 sub _items_previous {
1269 my $cust_main = $self->cust_main;
1270 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1272 foreach ( @pr_cust_bill ) {
1274 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1275 ' ('. time2str('%x',$_->_date). ')',
1276 #'pkgpart' => 'N/A',
1278 'amount' => sprintf("%10.2f", $_->owed),
1284 # 'description' => 'Previous Balance',
1285 # #'pkgpart' => 'N/A',
1286 # 'pkgnum' => 'N/A',
1287 # 'amount' => sprintf("%10.2f", $pr_total ),
1288 # 'ext_description' => [ map {
1289 # "Invoice ". $_->invnum.
1290 # " (". time2str("%x",$_->_date). ") ".
1291 # sprintf("%10.2f", $_->owed)
1292 # } @pr_cust_bill ],
1299 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1300 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1305 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1306 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1309 sub _items_cust_bill_pkg {
1311 my $cust_bill_pkg = shift;
1314 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1316 if ( $cust_bill_pkg->pkgnum ) {
1318 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1319 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1320 my $pkg = $part_pkg->pkg;
1323 #tie %labels, 'Tie::IxHash';
1324 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1325 my @ext_description;
1326 foreach my $label ( keys %labels ) {
1327 my @values = @{ $labels{$label} };
1328 my $num = scalar(@values);
1330 push @ext_description, "$label ($num)";
1332 push @ext_description, map { "$label: $_" } @values;
1336 if ( $cust_bill_pkg->setup != 0 ) {
1337 my $description = $pkg;
1338 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1339 my @d = @ext_description;
1340 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1342 'description' => $description,
1343 #'pkgpart' => $part_pkg->pkgpart,
1344 'pkgnum' => $cust_pkg->pkgnum,
1345 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1346 'ext_description' => \@d,
1350 if ( $cust_bill_pkg->recur != 0 ) {
1352 'description' => "$pkg (" .
1353 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1354 time2str('%x', $cust_bill_pkg->edate). ')',
1355 #'pkgpart' => $part_pkg->pkgpart,
1356 'pkgnum' => $cust_pkg->pkgnum,
1357 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1358 'ext_description' => [ @ext_description,
1359 $cust_bill_pkg->details,
1364 } else { #pkgnum tax or one-shot line item (??)
1366 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1367 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1369 if ( $cust_bill_pkg->setup != 0 ) {
1371 'description' => $itemdesc,
1372 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1375 if ( $cust_bill_pkg->recur != 0 ) {
1377 'description' => "$itemdesc (".
1378 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1379 time2str("%x", $cust_bill_pkg->edate). ')',
1380 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1392 sub _items_credits {
1397 foreach ( $self->cust_credited ) {
1399 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1401 my $reason = $_->cust_credit->reason;
1402 #my $reason = substr($_->cust_credit->reason,0,32);
1403 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1404 $reason = " ($reason) " if $reason;
1406 #'description' => 'Credit ref\#'. $_->crednum.
1407 # " (". time2str("%x",$_->cust_credit->_date) .")".
1409 'description' => 'Credit applied'.
1410 time2str("%x",$_->cust_credit->_date). $reason,
1411 'amount' => sprintf("%10.2f",$_->amount),
1414 #foreach ( @cr_cust_credit ) {
1416 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1417 # $money_char. sprintf("%10.2f",$_->credited)
1425 sub _items_payments {
1429 #get & print payments
1430 foreach ( $self->cust_bill_pay ) {
1432 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1435 'description' => "Payment received ".
1436 time2str("%x",$_->cust_pay->_date ),
1437 'amount' => sprintf("%10.2f", $_->amount )
1451 print_text formatting (and some logic :/) is in source, but needs to be
1452 slurped in from a file. Also number of lines ($=).
1456 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1457 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base