4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
8 use FS::UID qw( datasrc );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
12 use FS::cust_bill_pkg;
16 use FS::cust_credit_bill;
17 use FS::cust_pay_batch;
18 use FS::cust_bill_event;
20 @ISA = qw( FS::Record );
22 #ask FS::UID to run this stuff for us later
23 FS::UID->install_callback( sub {
25 $money_char = $conf->config('money_char') || '$';
30 FS::cust_bill - Object methods for cust_bill records
36 $record = new FS::cust_bill \%hash;
37 $record = new FS::cust_bill { 'column' => 'value' };
39 $error = $record->insert;
41 $error = $new_record->replace($old_record);
43 $error = $record->delete;
45 $error = $record->check;
47 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
49 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
51 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
53 @cust_pay_objects = $cust_bill->cust_pay;
55 $tax_amount = $record->tax;
57 @lines = $cust_bill->print_text;
58 @lines = $cust_bill->print_text $time;
62 An FS::cust_bill object represents an invoice; a declaration that a customer
63 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
64 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
65 following fields are currently supported:
69 =item invnum - primary key (assigned automatically for new invoices)
71 =item custnum - customer (see L<FS::cust_main>)
73 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
74 L<Time::Local> and L<Date::Parse> for conversion functions.
76 =item charged - amount of this invoice
78 =item printed - deprecated
80 =item closed - books closed flag, empty or `Y'
90 Creates a new invoice. To add the invoice to the database, see L<"insert">.
91 Invoices are normally created by calling the bill method of a customer object
92 (see L<FS::cust_main>).
96 sub table { 'cust_bill'; }
100 Adds this invoice to the database ("Posts" the invoice). If there is an error,
101 returns the error, otherwise returns false.
105 Currently unimplemented. I don't remove invoices because there would then be
106 no record you ever posted this invoice (which is bad, no?)
112 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
113 $self->SUPER::delete(@_);
116 =item replace OLD_RECORD
118 Replaces the OLD_RECORD with this one in the database. If there is an error,
119 returns the error, otherwise returns false.
121 Only printed may be changed. printed is normally updated by calling the
122 collect method of a customer object (see L<FS::cust_main>).
127 my( $new, $old ) = ( shift, shift );
128 return "Can't change custnum!" unless $old->custnum == $new->custnum;
129 #return "Can't change _date!" unless $old->_date eq $new->_date;
130 return "Can't change _date!" unless $old->_date == $new->_date;
131 return "Can't change charged!" unless $old->charged == $new->charged;
133 $new->SUPER::replace($old);
138 Checks all fields to make sure this is a valid invoice. If there is an error,
139 returns the error, otherwise returns false. Called by the insert and replace
148 $self->ut_numbern('invnum')
149 || $self->ut_number('custnum')
150 || $self->ut_numbern('_date')
151 || $self->ut_money('charged')
152 || $self->ut_numbern('printed')
153 || $self->ut_enum('closed', [ '', 'Y' ])
155 return $error if $error;
157 return "Unknown customer"
158 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
160 $self->_date(time) unless $self->_date;
162 $self->printed(0) if $self->printed eq '';
169 Returns a list consisting of the total previous balance for this customer,
170 followed by the previous outstanding invoices (as FS::cust_bill objects also).
177 my @cust_bill = sort { $a->_date <=> $b->_date }
178 grep { $_->owed != 0 && $_->_date < $self->_date }
179 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
181 foreach ( @cust_bill ) { $total += $_->owed; }
187 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
193 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
196 =item cust_bill_event
198 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
203 sub cust_bill_event {
205 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
211 Returns the customer (see L<FS::cust_main>) for this invoice.
217 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
222 Depreciated. See the cust_credited method.
224 #Returns a list consisting of the total previous credited (see
225 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
226 #outstanding credits (FS::cust_credit objects).
232 croak "FS::cust_bill->cust_credit depreciated; see ".
233 "FS::cust_bill->cust_credit_bill";
236 #my @cust_credit = sort { $a->_date <=> $b->_date }
237 # grep { $_->credited != 0 && $_->_date < $self->_date }
238 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
240 #foreach (@cust_credit) { $total += $_->credited; }
241 #$total, @cust_credit;
246 Depreciated. See the cust_bill_pay method.
248 #Returns all payments (see L<FS::cust_pay>) for this invoice.
254 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
256 #sort { $a->_date <=> $b->_date }
257 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
263 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
269 sort { $a->_date <=> $b->_date }
270 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
275 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
281 sort { $a->_date <=> $b->_date }
282 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
288 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
295 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
297 foreach (@taxlines) { $total += $_->setup; }
303 Returns the amount owed (still outstanding) on this invoice, which is charged
304 minus all payment applications (see L<FS::cust_bill_pay>) and credit
305 applications (see L<FS::cust_credit_bill>).
311 my $balance = $self->charged;
312 $balance -= $_->amount foreach ( $self->cust_bill_pay );
313 $balance -= $_->amount foreach ( $self->cust_credited );
314 $balance = sprintf( "%.2f", $balance);
315 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
321 Sends this invoice to the destinations configured for this customer: send
322 emails or print. See L<FS::cust_main_invoice>.
327 my($self,$template) = @_;
328 my @print_text = $self->print_text('', $template);
329 my @invoicing_list = $self->cust_main->invoicing_list;
331 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
333 #better to notify this person than silence
334 @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
336 my $error = send_email(
337 'from' => $conf->config('invoice_from'),
338 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
339 'subject' => 'Invoice',
340 'body' => \@print_text,
342 return "can't send invoice: $error" if $error;
346 if ( $conf->config('invoice_latex') ) {
347 @print_text = $self->print_ps('', $template);
350 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
351 my $lpr = $conf->config('lpr');
353 or return "Can't open pipe to $lpr: $!";
354 print LPR @print_text;
356 or return $! ? "Error closing $lpr: $!"
357 : "Exit status $? from $lpr";
364 =item send_csv OPTIONS
366 Sends invoice as a CSV data-file to a remote host with the specified protocol.
370 protocol - currently only "ftp"
376 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
377 and YYMMDDHHMMSS is a timestamp.
379 The fields of the CSV file is as follows:
381 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
385 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
387 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
388 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
389 fields are filled in.
391 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
392 first two fields (B<record_type> and B<invnum>) and the last five fields
393 (B<pkg> through B<edate>) are filled in.
395 =item invnum - invoice number
397 =item custnum - customer number
399 =item _date - invoice date
401 =item charged - total invoice amount
403 =item first - customer first name
405 =item last - customer first name
407 =item company - company name
409 =item address1 - address line 1
411 =item address2 - address line 1
421 =item pkg - line item description
423 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
425 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
427 =item sdate - start date for recurring fee
429 =item edate - end date for recurring fee
436 my($self, %opt) = @_;
438 #part one: create file
440 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
441 mkdir $spooldir, 0700 unless -d $spooldir;
443 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
445 open(CSV, ">$file") or die "can't open $file: $!";
447 eval "use Text::CSV_XS";
450 my $csv = Text::CSV_XS->new({'always_quote'=>1});
452 my $cust_main = $self->cust_main;
458 time2str("%x", $self->_date),
459 sprintf("%.2f", $self->charged),
460 ( map { $cust_main->getfield($_) }
461 qw( first last company address1 address2 city state zip country ) ),
463 ) or die "can't create csv";
464 print CSV $csv->string. "\n";
466 #new charges (false laziness w/print_text)
467 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
469 my($pkg, $setup, $recur, $sdate, $edate);
470 if ( $cust_bill_pkg->pkgnum ) {
472 ($pkg, $setup, $recur, $sdate, $edate) = (
473 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
474 ( $cust_bill_pkg->setup != 0
475 ? sprintf("%.2f", $cust_bill_pkg->setup )
477 ( $cust_bill_pkg->recur != 0
478 ? sprintf("%.2f", $cust_bill_pkg->recur )
480 time2str("%x", $cust_bill_pkg->sdate),
481 time2str("%x", $cust_bill_pkg->edate),
485 next unless $cust_bill_pkg->setup != 0;
486 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
487 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
489 ($pkg, $setup, $recur, $sdate, $edate) =
490 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
496 ( map { '' } (1..11) ),
497 ($pkg, $setup, $recur, $sdate, $edate)
498 ) or die "can't create csv";
499 print CSV $csv->string. "\n";
503 close CSV or die "can't close CSV: $!";
508 if ( $opt{protocol} eq 'ftp' ) {
509 eval "use Net::FTP;";
511 $net = Net::FTP->new($opt{server}) or die @$;
513 die "unknown protocol: $opt{protocol}";
516 $net->login( $opt{username}, $opt{password} )
517 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
519 $net->binary or die "can't set binary mode";
521 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
523 $net->put($file) or die "can't put $file: $!";
533 Pays this invoice with a compliemntary payment. If there is an error,
534 returns the error, otherwise returns false.
540 my $cust_pay = new FS::cust_pay ( {
541 'invnum' => $self->invnum,
542 'paid' => $self->owed,
545 'payinfo' => $self->cust_main->payinfo,
553 Attempts to pay this invoice with a credit card payment via a
554 Business::OnlinePayment realtime gateway. See
555 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
556 for supported processors.
562 $self->realtime_bop( 'CC', @_ );
567 Attempts to pay this invoice with an electronic check (ACH) payment via a
568 Business::OnlinePayment realtime gateway. See
569 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
570 for supported processors.
576 $self->realtime_bop( 'ECHECK', @_ );
581 Attempts to pay this invoice with phone bill (LEC) payment via a
582 Business::OnlinePayment realtime gateway. See
583 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
584 for supported processors.
590 $self->realtime_bop( 'LEC', @_ );
594 my( $self, $method ) = @_;
596 my $cust_main = $self->cust_main;
597 my $balance = $cust_main->balance;
598 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
599 $amount = sprintf("%.2f", $amount);
600 return "not run (balance $balance)" unless $amount > 0;
602 my $description = 'Internet Services';
603 if ( $conf->exists('business-onlinepayment-description') ) {
604 my $dtempl = $conf->config('business-onlinepayment-description');
606 my $agent_obj = $cust_main->agent
607 or die "can't retreive agent for $cust_main (agentnum ".
608 $cust_main->agentnum. ")";
609 my $agent = $agent_obj->agent;
610 my $pkgs = join(', ',
611 map { $_->cust_pkg->part_pkg->pkg }
612 grep { $_->pkgnum } $self->cust_bill_pkg
614 $description = eval qq("$dtempl");
617 $cust_main->realtime_bop($method, $amount,
618 'description' => $description,
619 'invnum' => $self->invnum,
626 Adds a payment for this invoice to the pending credit card batch (see
627 L<FS::cust_pay_batch>).
633 my $cust_main = $self->cust_main;
635 my $cust_pay_batch = new FS::cust_pay_batch ( {
636 'invnum' => $self->getfield('invnum'),
637 'custnum' => $cust_main->getfield('custnum'),
638 'last' => $cust_main->getfield('last'),
639 'first' => $cust_main->getfield('first'),
640 'address1' => $cust_main->getfield('address1'),
641 'address2' => $cust_main->getfield('address2'),
642 'city' => $cust_main->getfield('city'),
643 'state' => $cust_main->getfield('state'),
644 'zip' => $cust_main->getfield('zip'),
645 'country' => $cust_main->getfield('country'),
646 'cardnum' => $cust_main->getfield('payinfo'),
647 'exp' => $cust_main->getfield('paydate'),
648 'payname' => $cust_main->getfield('payname'),
649 'amount' => $self->owed,
651 my $error = $cust_pay_batch->insert;
652 die $error if $error;
657 =item print_text [ TIME [ , TEMPLATE ] ]
659 Returns an text invoice, as a list of lines.
661 TIME an optional value used to control the printing of overdue messages. The
662 default is now. It isn't the date of the invoice; that's the `_date' field.
663 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
664 L<Time::Local> and L<Date::Parse> for conversion functions.
670 my( $self, $today, $template ) = @_;
672 # my $invnum = $self->invnum;
673 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
674 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
675 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
677 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
678 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
679 #my $balance_due = $self->owed + $pr_total - $cr_total;
680 my $balance_due = $self->owed + $pr_total;
683 #my($description,$amount);
687 foreach ( @pr_cust_bill ) {
689 "Previous Balance, Invoice #". $_->invnum.
690 " (". time2str("%x",$_->_date). ")",
691 $money_char. sprintf("%10.2f",$_->owed)
695 push @buf,['','-----------'];
696 push @buf,[ 'Total Previous Balance',
697 $money_char. sprintf("%10.2f",$pr_total ) ];
702 foreach my $cust_bill_pkg (
703 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
704 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
707 if ( $cust_bill_pkg->pkgnum ) {
709 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
710 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
711 my $pkg = $part_pkg->pkg;
713 if ( $cust_bill_pkg->setup != 0 ) {
714 my $description = $pkg;
715 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
716 push @buf, [ $description,
717 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
719 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
722 if ( $cust_bill_pkg->recur != 0 ) {
724 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
725 time2str("%x", $cust_bill_pkg->edate) . ")",
726 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
729 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
732 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
734 } else { #pkgnum tax or one-shot line item
735 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
736 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
738 if ( $cust_bill_pkg->setup != 0 ) {
739 push @buf, [ $itemdesc,
740 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
742 if ( $cust_bill_pkg->recur != 0 ) {
743 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
744 . time2str("%x", $cust_bill_pkg->edate). ")",
745 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
751 push @buf,['','-----------'];
752 push @buf,['Total New Charges',
753 $money_char. sprintf("%10.2f",$self->charged) ];
756 push @buf,['','-----------'];
757 push @buf,['Total Charges',
758 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
762 foreach ( $self->cust_credited ) {
764 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
766 my $reason = substr($_->cust_credit->reason,0,32);
767 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
768 $reason = " ($reason) " if $reason;
770 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
772 $money_char. sprintf("%10.2f",$_->amount)
775 #foreach ( @cr_cust_credit ) {
777 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
778 # $money_char. sprintf("%10.2f",$_->credited)
782 #get & print payments
783 foreach ( $self->cust_bill_pay ) {
785 #something more elaborate if $_->amount ne ->cust_pay->paid ?
788 "Payment received ". time2str("%x",$_->cust_pay->_date ),
789 $money_char. sprintf("%10.2f",$_->amount )
794 my $balance_due_msg = $self->balance_due_msg;
796 push @buf,['','-----------'];
797 push @buf,[$balance_due_msg, $money_char.
798 sprintf("%10.2f", $balance_due ) ];
801 my $templatefile = 'invoice_template';
802 $templatefile .= "_$template" if $template;
803 my @invoice_template = $conf->config($templatefile)
804 or die "cannot load config file $templatefile";
807 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
808 /invoice_lines\((\d*)\)/;
809 $invoice_lines += $1 || scalar(@buf);
812 die "no invoice_lines() functions in template?" unless $wasfunc;
813 my $invoice_template = new Text::Template (
815 SOURCE => [ map "$_\n", @invoice_template ],
816 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
817 $invoice_template->compile()
818 or die "can't compile template: $Text::Template::ERROR";
820 #setup template variables
821 package FS::cust_bill::_template; #!
822 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
824 $invnum = $self->invnum;
825 $date = $self->_date;
827 $agent = $self->cust_main->agent->agent;
829 if ( $FS::cust_bill::invoice_lines ) {
831 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
833 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
838 #format address (variable for the template)
840 @address = ( '', '', '', '', '', '' );
841 package FS::cust_bill; #!
842 $FS::cust_bill::_template::address[$l++] =
844 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
845 ? " (P.O. #". $cust_main->payinfo. ")"
849 $FS::cust_bill::_template::address[$l++] = $cust_main->company
850 if $cust_main->company;
851 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
852 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
853 if $cust_main->address2;
854 $FS::cust_bill::_template::address[$l++] =
855 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
856 $FS::cust_bill::_template::address[$l++] = $cust_main->country
857 unless $cust_main->country eq 'US';
859 # #overdue? (variable for the template)
860 # $FS::cust_bill::_template::overdue = (
862 # && $today > $self->_date
863 ## && $self->printed > 1
864 # && $self->printed > 0
867 #and subroutine for the template
868 sub FS::cust_bill::_template::invoice_lines {
869 my $lines = shift || scalar(@buf);
871 scalar(@buf) ? shift @buf : [ '', '' ];
877 $FS::cust_bill::_template::page = 1;
881 push @collect, split("\n",
882 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
884 $FS::cust_bill::_template::page++;
887 map "$_\n", @collect;
891 =item print_ps [ TIME [ , TEMPLATE ] ]
893 Returns an postscript invoice, as a scalar.
895 TIME an optional value used to control the printing of overdue messages. The
896 default is now. It isn't the date of the invoice; that's the `_date' field.
897 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
898 L<Time::Local> and L<Date::Parse> for conversion functions.
902 #still some false laziness w/print_text
905 my( $self, $today, $template ) = @_;
908 # my $invnum = $self->invnum;
909 my $cust_main = $self->cust_main;
910 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
911 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
913 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
914 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
915 #my $balance_due = $self->owed + $pr_total - $cr_total;
916 my $balance_due = $self->owed + $pr_total;
919 #my($description,$amount);
923 my $templatefile = 'invoice_latex';
924 $templatefile .= "_$template" if $template;
925 my @invoice_template = $conf->config($templatefile)
926 or die "cannot load config file $templatefile";
929 'invnum' => $self->invnum,
930 'date' => time2str('%b %o, %Y', $self->_date),
931 'agent' => _latex_escape($cust_main->agent->agent),
932 'payname' => _latex_escape($cust_main->payname),
933 'company' => _latex_escape($cust_main->company),
934 'address1' => _latex_escape($cust_main->address1),
935 'address2' => _latex_escape($cust_main->address2),
936 'city' => _latex_escape($cust_main->city),
937 'state' => _latex_escape($cust_main->state),
938 'zip' => _latex_escape($cust_main->zip),
939 'country' => _latex_escape($cust_main->country),
940 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
942 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
943 'notes' => join("\n", $conf->config('invoice_latexnotes') ),
946 $invoice_data{'footer'} =~ s/\n+$//;
947 $invoice_data{'notes'} =~ s/\n+$//;
949 my $countrydefault = $conf->config('countrydefault') || 'US';
950 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
952 $invoice_data{'po_line'} =
953 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
954 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
960 while ( @invoice_template ) {
961 my $line = shift @invoice_template;
963 if ( $line =~ /^%%Detail\s*$/ ) {
965 while ( ( my $line_item_line = shift @invoice_template )
966 !~ /^%%EndDetail\s*$/ ) {
967 push @line_item, $line_item_line;
969 foreach my $line_item ( $self->_items ) {
970 #foreach my $line_item ( $self->_items_pkg ) {
971 $invoice_data{'ref'} = $line_item->{'pkgnum'};
972 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
973 if ( exists $line_item->{'ext_description'} ) {
974 $invoice_data{'description'} .=
975 "\\tabularnewline\n~~".
976 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
978 $invoice_data{'amount'} = $line_item->{'amount'};
979 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
981 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
984 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
986 while ( ( my $total_item_line = shift @invoice_template )
987 !~ /^%%EndTotalDetails\s*$/ ) {
988 push @total_item, $total_item_line;
994 foreach my $tax ( $self->_items_tax ) {
995 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
996 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
998 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1003 $invoice_data{'total_item'} = 'Sub-total';
1004 $invoice_data{'total_amount'} =
1005 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1006 unshift @total_fill,
1007 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1011 $invoice_data{'total_item'} = '\textbf{Total}';
1012 $invoice_data{'total_amount'} =
1013 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1015 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1018 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1021 foreach my $credit ( $self->_items_credits ) {
1022 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1024 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1026 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1031 foreach my $payment ( $self->_items_payments ) {
1032 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1034 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1036 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1040 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1041 $invoice_data{'total_amount'} =
1042 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1044 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1047 push @filled_in, @total_fill;
1050 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1051 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1052 push @filled_in, $line;
1063 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1064 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1067 my $file = $self->invnum. ".$unique";
1069 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1070 print TEX join("\n", @filled_in ), "\n";
1074 system('pslatex', "$file.tex");
1075 system('pslatex', "$file.tex");
1076 #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1077 system('dvips', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
1079 open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1081 #rm $file.dvi $file.log $file.aux
1082 #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1083 unlink("$file.dvi", "$file.log", "$file.aux");
1086 while (<POSTSCRIPT>) {
1096 # quick subroutine for print_ps
1098 # There are ten characters that LaTeX treats as special characters, which
1099 # means that they do not simply typeset themselves:
1100 # # $ % & ~ _ ^ \ { }
1102 # TeX ignores blanks following an escaped character; if you want a blank (as
1103 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1107 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1111 #utility methods for print_*
1113 sub balance_due_msg {
1115 my $msg = 'Balance Due';
1116 return $msg unless $conf->exists('invoice_default_terms');
1117 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1118 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1119 } elsif ( $conf->config('invoice_default_terms') ) {
1120 $msg .= ' - '. $conf->config('invoice_default_terms');
1127 my @display = scalar(@_)
1129 : qw( _items_previous _items_pkg );
1130 #: qw( _items_pkg );
1131 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1133 foreach my $display ( @display ) {
1134 push @b, $self->$display(@_);
1139 sub _items_previous {
1141 my $cust_main = $self->cust_main;
1142 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1144 foreach ( @pr_cust_bill ) {
1146 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1147 ' ('. time2str('%x',$_->_date). ')',
1148 #'pkgpart' => 'N/A',
1150 'amount' => sprintf("%10.2f", $_->owed),
1156 # 'description' => 'Previous Balance',
1157 # #'pkgpart' => 'N/A',
1158 # 'pkgnum' => 'N/A',
1159 # 'amount' => sprintf("%10.2f", $pr_total ),
1160 # 'ext_description' => [ map {
1161 # "Invoice ". $_->invnum.
1162 # " (". time2str("%x",$_->_date). ") ".
1163 # sprintf("%10.2f", $_->owed)
1164 # } @pr_cust_bill ],
1171 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1172 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1177 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1178 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1181 sub _items_cust_bill_pkg {
1183 my $cust_bill_pkg = shift;
1186 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1188 if ( $cust_bill_pkg->pkgnum ) {
1190 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1191 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1192 my $pkg = $part_pkg->pkg;
1194 if ( $cust_bill_pkg->setup != 0 ) {
1195 my $description = $pkg;
1196 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1198 @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1200 'description' => $description,
1201 #'pkgpart' => $part_pkg->pkgpart,
1202 'pkgnum' => $cust_pkg->pkgnum,
1203 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1204 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1205 $cust_pkg->labels ),
1211 if ( $cust_bill_pkg->recur != 0 ) {
1213 'description' => "$pkg (" .
1214 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1215 time2str('%x', $cust_bill_pkg->edate). ')',
1216 #'pkgpart' => $part_pkg->pkgpart,
1217 'pkgnum' => $cust_pkg->pkgnum,
1218 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1219 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1220 $cust_pkg->labels ),
1221 $cust_bill_pkg->details,
1226 } else { #pkgnum tax or one-shot line item (??)
1228 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1229 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1231 if ( $cust_bill_pkg->setup != 0 ) {
1233 'description' => $itemdesc,
1234 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1237 if ( $cust_bill_pkg->recur != 0 ) {
1239 'description' => "$itemdesc (".
1240 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1241 time2str("%x", $cust_bill_pkg->edate). ')',
1242 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1254 sub _items_credits {
1259 foreach ( $self->cust_credited ) {
1261 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1263 my $reason = $_->cust_credit->reason;
1264 #my $reason = substr($_->cust_credit->reason,0,32);
1265 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1266 $reason = " ($reason) " if $reason;
1268 #'description' => 'Credit ref\#'. $_->crednum.
1269 # " (". time2str("%x",$_->cust_credit->_date) .")".
1271 'description' => 'Credit applied'.
1272 time2str("%x",$_->cust_credit->_date). $reason,
1273 'amount' => sprintf("%10.2f",$_->amount),
1276 #foreach ( @cr_cust_credit ) {
1278 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1279 # $money_char. sprintf("%10.2f",$_->credited)
1287 sub _items_payments {
1291 #get & print payments
1292 foreach ( $self->cust_bill_pay ) {
1294 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1297 'description' => "Payment received ".
1298 time2str("%x",$_->cust_pay->_date ),
1299 'amount' => sprintf("%10.2f", $_->amount )
1313 print_text formatting (and some logic :/) is in source, but needs to be
1314 slurped in from a file. Also number of lines ($=).
1316 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1317 or something similar so the look can be completely customized?)
1321 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1322 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base