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_latex [ TIME [ , TEMPLATE ] ]
893 Internal method - returns a filename of a filled-in LaTeX template for this
894 invoice (Note: add ".tex" to get the actual filename).
896 See print_ps and print_pdf for methods that return PostScript and PDF output.
898 TIME an optional value used to control the printing of overdue messages. The
899 default is now. It isn't the date of the invoice; that's the `_date' field.
900 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
901 L<Time::Local> and L<Date::Parse> for conversion functions.
905 #still some false laziness w/print_text
908 my( $self, $today, $template ) = @_;
911 # my $invnum = $self->invnum;
912 my $cust_main = $self->cust_main;
913 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
914 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
916 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
917 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
918 #my $balance_due = $self->owed + $pr_total - $cr_total;
919 my $balance_due = $self->owed + $pr_total;
922 #my($description,$amount);
926 my $templatefile = 'invoice_latex';
927 $templatefile .= "_$template" if $template;
928 my @invoice_template = $conf->config($templatefile)
929 or die "cannot load config file $templatefile";
932 'invnum' => $self->invnum,
933 'date' => time2str('%b %o, %Y', $self->_date),
934 'agent' => _latex_escape($cust_main->agent->agent),
935 'payname' => _latex_escape($cust_main->payname),
936 'company' => _latex_escape($cust_main->company),
937 'address1' => _latex_escape($cust_main->address1),
938 'address2' => _latex_escape($cust_main->address2),
939 'city' => _latex_escape($cust_main->city),
940 'state' => _latex_escape($cust_main->state),
941 'zip' => _latex_escape($cust_main->zip),
942 'country' => _latex_escape($cust_main->country),
943 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
944 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
946 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
947 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
950 my $countrydefault = $conf->config('countrydefault') || 'US';
951 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
953 #do variable substitutions in notes
954 $invoice_data{'notes'} =
956 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
957 $conf->config('invoice_latexnotes')
960 $invoice_data{'footer'} =~ s/\n+$//;
961 $invoice_data{'smallfooter'} =~ s/\n+$//;
962 $invoice_data{'notes'} =~ s/\n+$//;
964 $invoice_data{'po_line'} =
965 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
966 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
972 while ( @invoice_template ) {
973 my $line = shift @invoice_template;
975 if ( $line =~ /^%%Detail\s*$/ ) {
977 while ( ( my $line_item_line = shift @invoice_template )
978 !~ /^%%EndDetail\s*$/ ) {
979 push @line_item, $line_item_line;
981 foreach my $line_item ( $self->_items ) {
982 #foreach my $line_item ( $self->_items_pkg ) {
983 $invoice_data{'ref'} = $line_item->{'pkgnum'};
984 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
985 if ( exists $line_item->{'ext_description'} ) {
986 $invoice_data{'description'} .=
987 "\\tabularnewline\n~~".
988 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
990 $invoice_data{'amount'} = $line_item->{'amount'};
991 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
993 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
996 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
998 while ( ( my $total_item_line = shift @invoice_template )
999 !~ /^%%EndTotalDetails\s*$/ ) {
1000 push @total_item, $total_item_line;
1003 my @total_fill = ();
1006 foreach my $tax ( $self->_items_tax ) {
1007 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1008 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1010 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1015 $invoice_data{'total_item'} = 'Sub-total';
1016 $invoice_data{'total_amount'} =
1017 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1018 unshift @total_fill,
1019 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1023 $invoice_data{'total_item'} = '\textbf{Total}';
1024 $invoice_data{'total_amount'} =
1025 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1027 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1030 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1033 foreach my $credit ( $self->_items_credits ) {
1034 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1036 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1038 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1043 foreach my $payment ( $self->_items_payments ) {
1044 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1046 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1048 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1052 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1053 $invoice_data{'total_amount'} =
1054 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1056 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1059 push @filled_in, @total_fill;
1062 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1063 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1064 push @filled_in, $line;
1075 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1076 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1079 my $file = $self->invnum. ".$unique";
1081 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1082 print TEX join("\n", @filled_in ), "\n";
1089 =item print_ps [ TIME [ , TEMPLATE ] ]
1091 Returns an postscript invoice, as a scalar.
1093 TIME an optional value used to control the printing of overdue messages. The
1094 default is now. It isn't the date of the invoice; that's the `_date' field.
1095 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1096 L<Time::Local> and L<Date::Parse> for conversion functions.
1103 my $file = $self->print_latex(@_);
1106 system('pslatex', "$file.tex");
1107 system('pslatex', "$file.tex");
1108 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
1110 open(POSTSCRIPT, "<$file.ps")
1111 or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1113 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1116 while (<POSTSCRIPT>) {
1126 =item print_pdf [ TIME [ , TEMPLATE ] ]
1128 Returns an PDF 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 #system('pdflatex', "$file.tex");
1143 #system('pdflatex', "$file.tex");
1144 #! LaTeX Error: Unknown graphics extension: .eps.
1147 system('pslatex', "$file.tex");
1148 system('pslatex', "$file.tex");
1150 #system('dvipdf', "$file.dvi", "$file.pdf" );
1151 system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -");
1153 open(PDF, "<$file.pdf")
1154 or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
1156 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1169 # quick subroutine for print_latex
1171 # There are ten characters that LaTeX treats as special characters, which
1172 # means that they do not simply typeset themselves:
1173 # # $ % & ~ _ ^ \ { }
1175 # TeX ignores blanks following an escaped character; if you want a blank (as
1176 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1180 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1184 #utility methods for print_*
1186 sub balance_due_msg {
1188 my $msg = 'Balance Due';
1189 return $msg unless $conf->exists('invoice_default_terms');
1190 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1191 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1192 } elsif ( $conf->config('invoice_default_terms') ) {
1193 $msg .= ' - '. $conf->config('invoice_default_terms');
1200 my @display = scalar(@_)
1202 : qw( _items_previous _items_pkg );
1203 #: qw( _items_pkg );
1204 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1206 foreach my $display ( @display ) {
1207 push @b, $self->$display(@_);
1212 sub _items_previous {
1214 my $cust_main = $self->cust_main;
1215 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1217 foreach ( @pr_cust_bill ) {
1219 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1220 ' ('. time2str('%x',$_->_date). ')',
1221 #'pkgpart' => 'N/A',
1223 'amount' => sprintf("%10.2f", $_->owed),
1229 # 'description' => 'Previous Balance',
1230 # #'pkgpart' => 'N/A',
1231 # 'pkgnum' => 'N/A',
1232 # 'amount' => sprintf("%10.2f", $pr_total ),
1233 # 'ext_description' => [ map {
1234 # "Invoice ". $_->invnum.
1235 # " (". time2str("%x",$_->_date). ") ".
1236 # sprintf("%10.2f", $_->owed)
1237 # } @pr_cust_bill ],
1244 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1245 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1250 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1251 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1254 sub _items_cust_bill_pkg {
1256 my $cust_bill_pkg = shift;
1259 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1261 if ( $cust_bill_pkg->pkgnum ) {
1263 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1264 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1265 my $pkg = $part_pkg->pkg;
1268 #tie %labels, 'Tie::IxHash';
1269 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1270 my @ext_description;
1271 foreach my $label ( keys %labels ) {
1272 my @values = @{ $labels{$label} };
1273 my $num = scalar(@values);
1275 push @ext_description, "$label ($num)";
1277 push @ext_description, map { "$label: $_" } @values;
1281 if ( $cust_bill_pkg->setup != 0 ) {
1282 my $description = $pkg;
1283 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1284 my @d = @ext_description;
1285 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1287 'description' => $description,
1288 #'pkgpart' => $part_pkg->pkgpart,
1289 'pkgnum' => $cust_pkg->pkgnum,
1290 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1291 'ext_description' => \@d,
1295 if ( $cust_bill_pkg->recur != 0 ) {
1297 'description' => "$pkg (" .
1298 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1299 time2str('%x', $cust_bill_pkg->edate). ')',
1300 #'pkgpart' => $part_pkg->pkgpart,
1301 'pkgnum' => $cust_pkg->pkgnum,
1302 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1303 'ext_description' => [ @ext_description,
1304 $cust_bill_pkg->details,
1309 } else { #pkgnum tax or one-shot line item (??)
1311 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1312 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1314 if ( $cust_bill_pkg->setup != 0 ) {
1316 'description' => $itemdesc,
1317 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1320 if ( $cust_bill_pkg->recur != 0 ) {
1322 'description' => "$itemdesc (".
1323 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1324 time2str("%x", $cust_bill_pkg->edate). ')',
1325 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1337 sub _items_credits {
1342 foreach ( $self->cust_credited ) {
1344 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1346 my $reason = $_->cust_credit->reason;
1347 #my $reason = substr($_->cust_credit->reason,0,32);
1348 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1349 $reason = " ($reason) " if $reason;
1351 #'description' => 'Credit ref\#'. $_->crednum.
1352 # " (". time2str("%x",$_->cust_credit->_date) .")".
1354 'description' => 'Credit applied'.
1355 time2str("%x",$_->cust_credit->_date). $reason,
1356 'amount' => sprintf("%10.2f",$_->amount),
1359 #foreach ( @cr_cust_credit ) {
1361 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1362 # $money_char. sprintf("%10.2f",$_->credited)
1370 sub _items_payments {
1374 #get & print payments
1375 foreach ( $self->cust_bill_pay ) {
1377 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1380 'description' => "Payment received ".
1381 time2str("%x",$_->cust_pay->_date ),
1382 'amount' => sprintf("%10.2f", $_->amount )
1396 print_text formatting (and some logic :/) is in source, but needs to be
1397 slurped in from a file. Also number of lines ($=).
1399 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1400 or something similar so the look can be completely customized?)
1404 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1405 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base