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
319 =item send [ TEMPLATENAME [ , AGENTNUM ] ]
321 Sends this invoice to the destinations configured for this customer: send
322 emails or print. See L<FS::cust_main_invoice>.
324 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
326 AGENTNUM, if specified, means that this invoice will only be sent for customers
327 of the specified agent.
333 my $template = scalar(@_) ? shift : '';
334 return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
336 my @print_text = $self->print_text('', $template);
337 my @invoicing_list = $self->cust_main->invoicing_list;
339 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
341 #better to notify this person than silence
342 @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
344 my $error = send_email(
345 'from' => $conf->config('invoice_from'),
346 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
347 'subject' => 'Invoice',
348 'body' => \@print_text,
350 die "can't email invoice: $error\n" if $error;
354 if ( $conf->config('invoice_latex') ) {
355 @print_text = $self->print_ps('', $template);
358 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
359 my $lpr = $conf->config('lpr');
361 or die "Can't open pipe to $lpr: $!\n";
362 print LPR @print_text;
364 or die $! ? "Error closing $lpr: $!\n"
365 : "Exit status $? from $lpr\n";
372 =item send_csv OPTIONS
374 Sends invoice as a CSV data-file to a remote host with the specified protocol.
378 protocol - currently only "ftp"
384 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
385 and YYMMDDHHMMSS is a timestamp.
387 The fields of the CSV file is as follows:
389 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
393 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
395 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
396 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
397 fields are filled in.
399 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
400 first two fields (B<record_type> and B<invnum>) and the last five fields
401 (B<pkg> through B<edate>) are filled in.
403 =item invnum - invoice number
405 =item custnum - customer number
407 =item _date - invoice date
409 =item charged - total invoice amount
411 =item first - customer first name
413 =item last - customer first name
415 =item company - company name
417 =item address1 - address line 1
419 =item address2 - address line 1
429 =item pkg - line item description
431 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
433 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
435 =item sdate - start date for recurring fee
437 =item edate - end date for recurring fee
444 my($self, %opt) = @_;
446 #part one: create file
448 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
449 mkdir $spooldir, 0700 unless -d $spooldir;
451 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
453 open(CSV, ">$file") or die "can't open $file: $!";
455 eval "use Text::CSV_XS";
458 my $csv = Text::CSV_XS->new({'always_quote'=>1});
460 my $cust_main = $self->cust_main;
466 time2str("%x", $self->_date),
467 sprintf("%.2f", $self->charged),
468 ( map { $cust_main->getfield($_) }
469 qw( first last company address1 address2 city state zip country ) ),
471 ) or die "can't create csv";
472 print CSV $csv->string. "\n";
474 #new charges (false laziness w/print_text)
475 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
477 my($pkg, $setup, $recur, $sdate, $edate);
478 if ( $cust_bill_pkg->pkgnum ) {
480 ($pkg, $setup, $recur, $sdate, $edate) = (
481 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
482 ( $cust_bill_pkg->setup != 0
483 ? sprintf("%.2f", $cust_bill_pkg->setup )
485 ( $cust_bill_pkg->recur != 0
486 ? sprintf("%.2f", $cust_bill_pkg->recur )
488 time2str("%x", $cust_bill_pkg->sdate),
489 time2str("%x", $cust_bill_pkg->edate),
493 next unless $cust_bill_pkg->setup != 0;
494 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
495 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
497 ($pkg, $setup, $recur, $sdate, $edate) =
498 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
504 ( map { '' } (1..11) ),
505 ($pkg, $setup, $recur, $sdate, $edate)
506 ) or die "can't create csv";
507 print CSV $csv->string. "\n";
511 close CSV or die "can't close CSV: $!";
516 if ( $opt{protocol} eq 'ftp' ) {
517 eval "use Net::FTP;";
519 $net = Net::FTP->new($opt{server}) or die @$;
521 die "unknown protocol: $opt{protocol}";
524 $net->login( $opt{username}, $opt{password} )
525 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
527 $net->binary or die "can't set binary mode";
529 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
531 $net->put($file) or die "can't put $file: $!";
541 Pays this invoice with a compliemntary payment. If there is an error,
542 returns the error, otherwise returns false.
548 my $cust_pay = new FS::cust_pay ( {
549 'invnum' => $self->invnum,
550 'paid' => $self->owed,
553 'payinfo' => $self->cust_main->payinfo,
561 Attempts to pay this invoice with a credit card payment via a
562 Business::OnlinePayment realtime gateway. See
563 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
564 for supported processors.
570 $self->realtime_bop( 'CC', @_ );
575 Attempts to pay this invoice with an electronic check (ACH) payment via a
576 Business::OnlinePayment realtime gateway. See
577 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
578 for supported processors.
584 $self->realtime_bop( 'ECHECK', @_ );
589 Attempts to pay this invoice with phone bill (LEC) payment via a
590 Business::OnlinePayment realtime gateway. See
591 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
592 for supported processors.
598 $self->realtime_bop( 'LEC', @_ );
602 my( $self, $method ) = @_;
604 my $cust_main = $self->cust_main;
605 my $balance = $cust_main->balance;
606 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
607 $amount = sprintf("%.2f", $amount);
608 return "not run (balance $balance)" unless $amount > 0;
610 my $description = 'Internet Services';
611 if ( $conf->exists('business-onlinepayment-description') ) {
612 my $dtempl = $conf->config('business-onlinepayment-description');
614 my $agent_obj = $cust_main->agent
615 or die "can't retreive agent for $cust_main (agentnum ".
616 $cust_main->agentnum. ")";
617 my $agent = $agent_obj->agent;
618 my $pkgs = join(', ',
619 map { $_->cust_pkg->part_pkg->pkg }
620 grep { $_->pkgnum } $self->cust_bill_pkg
622 $description = eval qq("$dtempl");
625 $cust_main->realtime_bop($method, $amount,
626 'description' => $description,
627 'invnum' => $self->invnum,
634 Adds a payment for this invoice to the pending credit card batch (see
635 L<FS::cust_pay_batch>).
641 my $cust_main = $self->cust_main;
643 my $cust_pay_batch = new FS::cust_pay_batch ( {
644 'invnum' => $self->getfield('invnum'),
645 'custnum' => $cust_main->getfield('custnum'),
646 'last' => $cust_main->getfield('last'),
647 'first' => $cust_main->getfield('first'),
648 'address1' => $cust_main->getfield('address1'),
649 'address2' => $cust_main->getfield('address2'),
650 'city' => $cust_main->getfield('city'),
651 'state' => $cust_main->getfield('state'),
652 'zip' => $cust_main->getfield('zip'),
653 'country' => $cust_main->getfield('country'),
654 'cardnum' => $cust_main->getfield('payinfo'),
655 'exp' => $cust_main->getfield('paydate'),
656 'payname' => $cust_main->getfield('payname'),
657 'amount' => $self->owed,
659 my $error = $cust_pay_batch->insert;
660 die $error if $error;
665 sub _agent_template {
668 my $cust_bill_event = qsearchs( 'part_bill_event',
670 'payby' => $self->cust_main->payby,
671 'plan' => 'send_agent',
672 'eventcode' => { 'op' => 'LIKE',
673 'value' => '_%, '. $self->cust_main->agentnum. ');' },
676 'ORDER BY seconds LIMIT 1'
679 return '' unless $cust_bill_event;
681 if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
684 warn "can't parse eventcode for agent-specific invoice template";
690 =item print_text [ TIME [ , TEMPLATE ] ]
692 Returns an text invoice, as a list of lines.
694 TIME an optional value used to control the printing of overdue messages. The
695 default is now. It isn't the date of the invoice; that's the `_date' field.
696 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
697 L<Time::Local> and L<Date::Parse> for conversion functions.
703 my( $self, $today, $template ) = @_;
705 # my $invnum = $self->invnum;
706 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
707 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
708 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
710 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
711 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
712 #my $balance_due = $self->owed + $pr_total - $cr_total;
713 my $balance_due = $self->owed + $pr_total;
716 #my($description,$amount);
720 foreach ( @pr_cust_bill ) {
722 "Previous Balance, Invoice #". $_->invnum.
723 " (". time2str("%x",$_->_date). ")",
724 $money_char. sprintf("%10.2f",$_->owed)
728 push @buf,['','-----------'];
729 push @buf,[ 'Total Previous Balance',
730 $money_char. sprintf("%10.2f",$pr_total ) ];
735 foreach my $cust_bill_pkg (
736 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
737 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
740 if ( $cust_bill_pkg->pkgnum ) {
742 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
743 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
744 my $pkg = $part_pkg->pkg;
746 if ( $cust_bill_pkg->setup != 0 ) {
747 my $description = $pkg;
748 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
749 push @buf, [ $description,
750 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
752 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
755 if ( $cust_bill_pkg->recur != 0 ) {
757 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
758 time2str("%x", $cust_bill_pkg->edate) . ")",
759 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
762 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
765 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
767 } else { #pkgnum tax or one-shot line item
768 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
769 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
771 if ( $cust_bill_pkg->setup != 0 ) {
772 push @buf, [ $itemdesc,
773 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
775 if ( $cust_bill_pkg->recur != 0 ) {
776 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
777 . time2str("%x", $cust_bill_pkg->edate). ")",
778 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
784 push @buf,['','-----------'];
785 push @buf,['Total New Charges',
786 $money_char. sprintf("%10.2f",$self->charged) ];
789 push @buf,['','-----------'];
790 push @buf,['Total Charges',
791 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
795 foreach ( $self->cust_credited ) {
797 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
799 my $reason = substr($_->cust_credit->reason,0,32);
800 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
801 $reason = " ($reason) " if $reason;
803 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
805 $money_char. sprintf("%10.2f",$_->amount)
808 #foreach ( @cr_cust_credit ) {
810 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
811 # $money_char. sprintf("%10.2f",$_->credited)
815 #get & print payments
816 foreach ( $self->cust_bill_pay ) {
818 #something more elaborate if $_->amount ne ->cust_pay->paid ?
821 "Payment received ". time2str("%x",$_->cust_pay->_date ),
822 $money_char. sprintf("%10.2f",$_->amount )
827 my $balance_due_msg = $self->balance_due_msg;
829 push @buf,['','-----------'];
830 push @buf,[$balance_due_msg, $money_char.
831 sprintf("%10.2f", $balance_due ) ];
834 $template ||= $self->_agent_template;
835 my $templatefile = 'invoice_template';
836 $templatefile .= "_$template" if length($template);
837 my @invoice_template = $conf->config($templatefile)
838 or die "cannot load config file $templatefile";
841 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
842 /invoice_lines\((\d*)\)/;
843 $invoice_lines += $1 || scalar(@buf);
846 die "no invoice_lines() functions in template?" unless $wasfunc;
847 my $invoice_template = new Text::Template (
849 SOURCE => [ map "$_\n", @invoice_template ],
850 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
851 $invoice_template->compile()
852 or die "can't compile template: $Text::Template::ERROR";
854 #setup template variables
855 package FS::cust_bill::_template; #!
856 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
858 $invnum = $self->invnum;
859 $date = $self->_date;
861 $agent = $self->cust_main->agent->agent;
863 if ( $FS::cust_bill::invoice_lines ) {
865 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
867 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
872 #format address (variable for the template)
874 @address = ( '', '', '', '', '', '' );
875 package FS::cust_bill; #!
876 $FS::cust_bill::_template::address[$l++] =
878 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
879 ? " (P.O. #". $cust_main->payinfo. ")"
883 $FS::cust_bill::_template::address[$l++] = $cust_main->company
884 if $cust_main->company;
885 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
886 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
887 if $cust_main->address2;
888 $FS::cust_bill::_template::address[$l++] =
889 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
890 $FS::cust_bill::_template::address[$l++] = $cust_main->country
891 unless $cust_main->country eq 'US';
893 # #overdue? (variable for the template)
894 # $FS::cust_bill::_template::overdue = (
896 # && $today > $self->_date
897 ## && $self->printed > 1
898 # && $self->printed > 0
901 #and subroutine for the template
902 sub FS::cust_bill::_template::invoice_lines {
903 my $lines = shift || scalar(@buf);
905 scalar(@buf) ? shift @buf : [ '', '' ];
911 $FS::cust_bill::_template::page = 1;
915 push @collect, split("\n",
916 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
918 $FS::cust_bill::_template::page++;
921 map "$_\n", @collect;
925 =item print_latex [ TIME [ , TEMPLATE ] ]
927 Internal method - returns a filename of a filled-in LaTeX template for this
928 invoice (Note: add ".tex" to get the actual filename).
930 See print_ps and print_pdf for methods that return PostScript and PDF output.
932 TIME an optional value used to control the printing of overdue messages. The
933 default is now. It isn't the date of the invoice; that's the `_date' field.
934 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
935 L<Time::Local> and L<Date::Parse> for conversion functions.
939 #still some false laziness w/print_text
942 my( $self, $today, $template ) = @_;
945 # my $invnum = $self->invnum;
946 my $cust_main = $self->cust_main;
947 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
948 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
950 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
951 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
952 #my $balance_due = $self->owed + $pr_total - $cr_total;
953 my $balance_due = $self->owed + $pr_total;
956 #my($description,$amount);
960 $template ||= $self->_agent_template;
961 my $templatefile = 'invoice_latex';
962 my $suffix = length($template) ? "_$template" : '';
963 $templatefile .= $suffix;
964 my @invoice_template = $conf->config($templatefile)
965 or die "cannot load config file $templatefile";
968 'invnum' => $self->invnum,
969 'date' => time2str('%b %o, %Y', $self->_date),
970 'agent' => _latex_escape($cust_main->agent->agent),
971 'payname' => _latex_escape($cust_main->payname),
972 'company' => _latex_escape($cust_main->company),
973 'address1' => _latex_escape($cust_main->address1),
974 'address2' => _latex_escape($cust_main->address2),
975 'city' => _latex_escape($cust_main->city),
976 'state' => _latex_escape($cust_main->state),
977 'zip' => _latex_escape($cust_main->zip),
978 'country' => _latex_escape($cust_main->country),
979 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
980 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
982 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
983 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
986 my $countrydefault = $conf->config('countrydefault') || 'US';
987 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
989 #do variable substitutions in notes
990 $invoice_data{'notes'} =
992 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
993 $conf->config_orbase('invoice_latexnotes', $suffix)
996 $invoice_data{'footer'} =~ s/\n+$//;
997 $invoice_data{'smallfooter'} =~ s/\n+$//;
998 $invoice_data{'notes'} =~ s/\n+$//;
1000 $invoice_data{'po_line'} =
1001 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1002 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1006 my @total_item = ();
1008 while ( @invoice_template ) {
1009 my $line = shift @invoice_template;
1011 if ( $line =~ /^%%Detail\s*$/ ) {
1013 while ( ( my $line_item_line = shift @invoice_template )
1014 !~ /^%%EndDetail\s*$/ ) {
1015 push @line_item, $line_item_line;
1017 foreach my $line_item ( $self->_items ) {
1018 #foreach my $line_item ( $self->_items_pkg ) {
1019 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1020 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1021 if ( exists $line_item->{'ext_description'} ) {
1022 $invoice_data{'description'} .=
1023 "\\tabularnewline\n~~".
1024 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1026 $invoice_data{'amount'} = $line_item->{'amount'};
1027 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1029 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1032 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1034 while ( ( my $total_item_line = shift @invoice_template )
1035 !~ /^%%EndTotalDetails\s*$/ ) {
1036 push @total_item, $total_item_line;
1039 my @total_fill = ();
1042 foreach my $tax ( $self->_items_tax ) {
1043 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1044 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1046 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1051 $invoice_data{'total_item'} = 'Sub-total';
1052 $invoice_data{'total_amount'} =
1053 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1054 unshift @total_fill,
1055 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1059 $invoice_data{'total_item'} = '\textbf{Total}';
1060 $invoice_data{'total_amount'} =
1061 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1063 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1066 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1069 foreach my $credit ( $self->_items_credits ) {
1070 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1072 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1074 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1079 foreach my $payment ( $self->_items_payments ) {
1080 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1082 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1084 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1088 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1089 $invoice_data{'total_amount'} =
1090 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1092 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1095 push @filled_in, @total_fill;
1098 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1099 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1100 push @filled_in, $line;
1111 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1112 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1115 my $file = $self->invnum. ".$unique";
1117 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1118 print TEX join("\n", @filled_in ), "\n";
1125 =item print_ps [ TIME [ , TEMPLATE ] ]
1127 Returns an postscript invoice, as a scalar.
1129 TIME an optional value used to control the printing of overdue messages. The
1130 default is now. It isn't the date of the invoice; that's the `_date' field.
1131 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1132 L<Time::Local> and L<Date::Parse> for conversion functions.
1139 my $file = $self->print_latex(@_);
1142 system('pslatex', "$file.tex");
1143 system('pslatex', "$file.tex");
1144 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
1146 open(POSTSCRIPT, "<$file.ps")
1147 or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1149 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1152 while (<POSTSCRIPT>) {
1162 =item print_pdf [ TIME [ , TEMPLATE ] ]
1164 Returns an PDF invoice, as a scalar.
1166 TIME an optional value used to control the printing of overdue messages. The
1167 default is now. It isn't the date of the invoice; that's the `_date' field.
1168 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1169 L<Time::Local> and L<Date::Parse> for conversion functions.
1176 my $file = $self->print_latex(@_);
1178 #system('pdflatex', "$file.tex");
1179 #system('pdflatex', "$file.tex");
1180 #! LaTeX Error: Unknown graphics extension: .eps.
1183 system('pslatex', "$file.tex");
1184 system('pslatex', "$file.tex");
1186 #system('dvipdf', "$file.dvi", "$file.pdf" );
1187 system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -");
1189 open(PDF, "<$file.pdf")
1190 or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
1192 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1205 # quick subroutine for print_latex
1207 # There are ten characters that LaTeX treats as special characters, which
1208 # means that they do not simply typeset themselves:
1209 # # $ % & ~ _ ^ \ { }
1211 # TeX ignores blanks following an escaped character; if you want a blank (as
1212 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1216 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1220 #utility methods for print_*
1222 sub balance_due_msg {
1224 my $msg = 'Balance Due';
1225 return $msg unless $conf->exists('invoice_default_terms');
1226 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1227 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1228 } elsif ( $conf->config('invoice_default_terms') ) {
1229 $msg .= ' - '. $conf->config('invoice_default_terms');
1236 my @display = scalar(@_)
1238 : qw( _items_previous _items_pkg );
1239 #: qw( _items_pkg );
1240 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1242 foreach my $display ( @display ) {
1243 push @b, $self->$display(@_);
1248 sub _items_previous {
1250 my $cust_main = $self->cust_main;
1251 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1253 foreach ( @pr_cust_bill ) {
1255 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1256 ' ('. time2str('%x',$_->_date). ')',
1257 #'pkgpart' => 'N/A',
1259 'amount' => sprintf("%10.2f", $_->owed),
1265 # 'description' => 'Previous Balance',
1266 # #'pkgpart' => 'N/A',
1267 # 'pkgnum' => 'N/A',
1268 # 'amount' => sprintf("%10.2f", $pr_total ),
1269 # 'ext_description' => [ map {
1270 # "Invoice ". $_->invnum.
1271 # " (". time2str("%x",$_->_date). ") ".
1272 # sprintf("%10.2f", $_->owed)
1273 # } @pr_cust_bill ],
1280 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1281 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1286 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1287 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1290 sub _items_cust_bill_pkg {
1292 my $cust_bill_pkg = shift;
1295 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1297 if ( $cust_bill_pkg->pkgnum ) {
1299 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1300 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1301 my $pkg = $part_pkg->pkg;
1304 #tie %labels, 'Tie::IxHash';
1305 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1306 my @ext_description;
1307 foreach my $label ( keys %labels ) {
1308 my @values = @{ $labels{$label} };
1309 my $num = scalar(@values);
1311 push @ext_description, "$label ($num)";
1313 push @ext_description, map { "$label: $_" } @values;
1317 if ( $cust_bill_pkg->setup != 0 ) {
1318 my $description = $pkg;
1319 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1320 my @d = @ext_description;
1321 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1323 'description' => $description,
1324 #'pkgpart' => $part_pkg->pkgpart,
1325 'pkgnum' => $cust_pkg->pkgnum,
1326 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1327 'ext_description' => \@d,
1331 if ( $cust_bill_pkg->recur != 0 ) {
1333 'description' => "$pkg (" .
1334 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1335 time2str('%x', $cust_bill_pkg->edate). ')',
1336 #'pkgpart' => $part_pkg->pkgpart,
1337 'pkgnum' => $cust_pkg->pkgnum,
1338 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1339 'ext_description' => [ @ext_description,
1340 $cust_bill_pkg->details,
1345 } else { #pkgnum tax or one-shot line item (??)
1347 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1348 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1350 if ( $cust_bill_pkg->setup != 0 ) {
1352 'description' => $itemdesc,
1353 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1356 if ( $cust_bill_pkg->recur != 0 ) {
1358 'description' => "$itemdesc (".
1359 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1360 time2str("%x", $cust_bill_pkg->edate). ')',
1361 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1373 sub _items_credits {
1378 foreach ( $self->cust_credited ) {
1380 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1382 my $reason = $_->cust_credit->reason;
1383 #my $reason = substr($_->cust_credit->reason,0,32);
1384 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1385 $reason = " ($reason) " if $reason;
1387 #'description' => 'Credit ref\#'. $_->crednum.
1388 # " (". time2str("%x",$_->cust_credit->_date) .")".
1390 'description' => 'Credit applied'.
1391 time2str("%x",$_->cust_credit->_date). $reason,
1392 'amount' => sprintf("%10.2f",$_->amount),
1395 #foreach ( @cr_cust_credit ) {
1397 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1398 # $money_char. sprintf("%10.2f",$_->credited)
1406 sub _items_payments {
1410 #get & print payments
1411 foreach ( $self->cust_bill_pay ) {
1413 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1416 'description' => "Payment received ".
1417 time2str("%x",$_->cust_pay->_date ),
1418 'amount' => sprintf("%10.2f", $_->amount )
1432 print_text formatting (and some logic :/) is in source, but needs to be
1433 slurped in from a file. Also number of lines ($=).
1437 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1438 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base