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 $amount = $self->owed;
599 my $description = 'Internet Services';
600 if ( $conf->exists('business-onlinepayment-description') ) {
601 my $dtempl = $conf->config('business-onlinepayment-description');
603 my $agent_obj = $cust_main->agent
604 or die "can't retreive agent for $cust_main (agentnum ".
605 $cust_main->agentnum. ")";
606 my $agent = $agent_obj->agent;
607 my $pkgs = join(', ',
608 map { $_->cust_pkg->part_pkg->pkg }
609 grep { $_->pkgnum } $self->cust_bill_pkg
611 $description = eval qq("$dtempl");
614 $cust_main->realtime_bop($method, $amount,
615 'description' => $description,
616 'invnum' => $self->invnum,
623 Adds a payment for this invoice to the pending credit card batch (see
624 L<FS::cust_pay_batch>).
630 my $cust_main = $self->cust_main;
632 my $cust_pay_batch = new FS::cust_pay_batch ( {
633 'invnum' => $self->getfield('invnum'),
634 'custnum' => $cust_main->getfield('custnum'),
635 'last' => $cust_main->getfield('last'),
636 'first' => $cust_main->getfield('first'),
637 'address1' => $cust_main->getfield('address1'),
638 'address2' => $cust_main->getfield('address2'),
639 'city' => $cust_main->getfield('city'),
640 'state' => $cust_main->getfield('state'),
641 'zip' => $cust_main->getfield('zip'),
642 'country' => $cust_main->getfield('country'),
643 'cardnum' => $cust_main->getfield('payinfo'),
644 'exp' => $cust_main->getfield('paydate'),
645 'payname' => $cust_main->getfield('payname'),
646 'amount' => $self->owed,
648 my $error = $cust_pay_batch->insert;
649 die $error if $error;
654 =item print_text [ TIME [ , TEMPLATE ] ]
656 Returns an text invoice, as a list of lines.
658 TIME an optional value used to control the printing of overdue messages. The
659 default is now. It isn't the date of the invoice; that's the `_date' field.
660 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
661 L<Time::Local> and L<Date::Parse> for conversion functions.
667 my( $self, $today, $template ) = @_;
669 # my $invnum = $self->invnum;
670 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
671 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
672 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
674 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
675 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
676 #my $balance_due = $self->owed + $pr_total - $cr_total;
677 my $balance_due = $self->owed + $pr_total;
680 #my($description,$amount);
684 foreach ( @pr_cust_bill ) {
686 "Previous Balance, Invoice #". $_->invnum.
687 " (". time2str("%x",$_->_date). ")",
688 $money_char. sprintf("%10.2f",$_->owed)
692 push @buf,['','-----------'];
693 push @buf,[ 'Total Previous Balance',
694 $money_char. sprintf("%10.2f",$pr_total ) ];
699 foreach my $cust_bill_pkg (
700 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
701 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
704 if ( $cust_bill_pkg->pkgnum ) {
706 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
707 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
708 my $pkg = $part_pkg->pkg;
710 if ( $cust_bill_pkg->setup != 0 ) {
711 my $description = $pkg;
712 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
713 push @buf, [ $description,
714 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
716 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
719 if ( $cust_bill_pkg->recur != 0 ) {
721 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
722 time2str("%x", $cust_bill_pkg->edate) . ")",
723 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
726 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
729 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
731 } else { #pkgnum tax or one-shot line item
732 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
733 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
735 if ( $cust_bill_pkg->setup != 0 ) {
736 push @buf, [ $itemdesc,
737 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
739 if ( $cust_bill_pkg->recur != 0 ) {
740 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
741 . time2str("%x", $cust_bill_pkg->edate). ")",
742 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
748 push @buf,['','-----------'];
749 push @buf,['Total New Charges',
750 $money_char. sprintf("%10.2f",$self->charged) ];
753 push @buf,['','-----------'];
754 push @buf,['Total Charges',
755 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
759 foreach ( $self->cust_credited ) {
761 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
763 my $reason = substr($_->cust_credit->reason,0,32);
764 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
765 $reason = " ($reason) " if $reason;
767 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
769 $money_char. sprintf("%10.2f",$_->amount)
772 #foreach ( @cr_cust_credit ) {
774 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
775 # $money_char. sprintf("%10.2f",$_->credited)
779 #get & print payments
780 foreach ( $self->cust_bill_pay ) {
782 #something more elaborate if $_->amount ne ->cust_pay->paid ?
785 "Payment received ". time2str("%x",$_->cust_pay->_date ),
786 $money_char. sprintf("%10.2f",$_->amount )
791 my $balance_due_msg = $self->balance_due_msg;
793 push @buf,['','-----------'];
794 push @buf,[$balance_due_msg, $money_char.
795 sprintf("%10.2f", $balance_due ) ];
798 my $templatefile = 'invoice_template';
799 $templatefile .= "_$template" if $template;
800 my @invoice_template = $conf->config($templatefile)
801 or die "cannot load config file $templatefile";
804 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
805 /invoice_lines\((\d*)\)/;
806 $invoice_lines += $1 || scalar(@buf);
809 die "no invoice_lines() functions in template?" unless $wasfunc;
810 my $invoice_template = new Text::Template (
812 SOURCE => [ map "$_\n", @invoice_template ],
813 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
814 $invoice_template->compile()
815 or die "can't compile template: $Text::Template::ERROR";
817 #setup template variables
818 package FS::cust_bill::_template; #!
819 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
821 $invnum = $self->invnum;
822 $date = $self->_date;
824 $agent = $self->cust_main->agent->agent;
826 if ( $FS::cust_bill::invoice_lines ) {
828 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
830 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
835 #format address (variable for the template)
837 @address = ( '', '', '', '', '', '' );
838 package FS::cust_bill; #!
839 $FS::cust_bill::_template::address[$l++] =
841 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
842 ? " (P.O. #". $cust_main->payinfo. ")"
846 $FS::cust_bill::_template::address[$l++] = $cust_main->company
847 if $cust_main->company;
848 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
849 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
850 if $cust_main->address2;
851 $FS::cust_bill::_template::address[$l++] =
852 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
853 $FS::cust_bill::_template::address[$l++] = $cust_main->country
854 unless $cust_main->country eq 'US';
856 # #overdue? (variable for the template)
857 # $FS::cust_bill::_template::overdue = (
859 # && $today > $self->_date
860 ## && $self->printed > 1
861 # && $self->printed > 0
864 #and subroutine for the template
865 sub FS::cust_bill::_template::invoice_lines {
866 my $lines = shift || scalar(@buf);
868 scalar(@buf) ? shift @buf : [ '', '' ];
874 $FS::cust_bill::_template::page = 1;
878 push @collect, split("\n",
879 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
881 $FS::cust_bill::_template::page++;
884 map "$_\n", @collect;
888 =item print_ps [ TIME [ , TEMPLATE ] ]
890 Returns an postscript invoice, as a scalar.
892 TIME an optional value used to control the printing of overdue messages. The
893 default is now. It isn't the date of the invoice; that's the `_date' field.
894 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
895 L<Time::Local> and L<Date::Parse> for conversion functions.
899 #still some false laziness w/print_text
902 my( $self, $today, $template ) = @_;
905 # my $invnum = $self->invnum;
906 my $cust_main = $self->cust_main;
907 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
908 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
910 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
911 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
912 #my $balance_due = $self->owed + $pr_total - $cr_total;
913 my $balance_due = $self->owed + $pr_total;
916 #my($description,$amount);
920 my $templatefile = 'invoice_latex';
921 $templatefile .= "_$template" if $template;
922 my @invoice_template = $conf->config($templatefile)
923 or die "cannot load config file $templatefile";
926 'invnum' => $self->invnum,
927 'date' => time2str('%b %o, %Y', $self->_date),
928 'agent' => $cust_main->agent->agent,
929 'payname' => $cust_main->payname,
930 'company' => $cust_main->company,
931 'address1' => $cust_main->address1,
932 'address2' => $cust_main->address2,
933 'city' => $cust_main->city,
934 'state' => $cust_main->state,
935 'zip' => $cust_main->zip,
936 'country' => $cust_main->country,
937 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
939 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
940 'notes' => join("\n", $conf->config('invoice_latexnotes') ),
943 $invoice_data{'footer'} =~ s/\n+$//;
944 $invoice_data{'notes'} =~ s/\n+$//;
946 my $countrydefault = $conf->config('countrydefault') || 'US';
947 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
949 $invoice_data{'po_line'} =
950 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
951 ? "Purchase Order #". $cust_main->payinfo
957 while ( @invoice_template ) {
958 my $line = shift @invoice_template;
960 if ( $line =~ /^%%Detail\s*$/ ) {
962 while ( ( my $line_item_line = shift @invoice_template )
963 !~ /^%%EndDetail\s*$/ ) {
964 push @line_item, $line_item_line;
966 foreach my $line_item ( $self->_items ) {
967 #foreach my $line_item ( $self->_items_pkg ) {
968 $invoice_data{'ref'} = $line_item->{'pkgnum'};
969 $invoice_data{'description'} = $line_item->{'description'};
970 if ( exists $line_item->{'ext_description'} ) {
971 $invoice_data{'description'} .=
972 "\\tabularnewline\n~~".
973 join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
975 $invoice_data{'amount'} = $line_item->{'amount'};
976 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
978 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
981 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
983 while ( ( my $total_item_line = shift @invoice_template )
984 !~ /^%%EndTotalDetails\s*$/ ) {
985 push @total_item, $total_item_line;
991 foreach my $tax ( $self->_items_tax ) {
992 $invoice_data{'total_item'} = $tax->{'description'};
993 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
995 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1000 $invoice_data{'total_item'} = 'Sub-total';
1001 $invoice_data{'total_amount'} =
1002 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1003 unshift @total_fill,
1004 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1008 $invoice_data{'total_item'} = '\textbf{Total}';
1009 $invoice_data{'total_amount'} =
1010 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1012 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1015 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1018 foreach my $credit ( $self->_items_credits ) {
1019 $invoice_data{'total_item'} = $credit->{'description'};
1021 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1023 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1028 foreach my $payment ( $self->_items_payments ) {
1029 $invoice_data{'total_item'} = $payment->{'description'};
1031 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1033 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1037 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1038 $invoice_data{'total_amount'} =
1039 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1041 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1044 push @filled_in, @total_fill;
1047 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1048 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1049 push @filled_in, $line;
1060 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1061 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1064 my $file = $self->invnum. ".$unique";
1066 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1067 print TEX join("\n", @filled_in ), "\n";
1071 system('pslatex', "$file.tex");
1072 system('pslatex', "$file.tex");
1073 #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1074 system('dvips', '-t', 'letter', "$file.dvi" );
1076 open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1078 #rm $file.dvi $file.log $file.aux
1079 #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1080 unlink("$file.dvi", "$file.log", "$file.aux");
1083 while (<POSTSCRIPT>) {
1093 #utility methods for print_*
1095 sub balance_due_msg {
1097 my $msg = 'Balance Due';
1098 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1099 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1100 } elsif ( $conf->config('invoice_default_terms') ) {
1101 $msg .= ' - '. $conf->config('invoice_default_terms');
1108 my @display = scalar(@_)
1110 : qw( _items_previous _items_pkg );
1111 #: qw( _items_pkg );
1112 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1114 foreach my $display ( @display ) {
1115 push @b, $self->$display(@_);
1120 sub _items_previous {
1122 my $cust_main = $self->cust_main;
1123 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1125 foreach ( @pr_cust_bill ) {
1127 'description' => 'Previous Balance, Invoice \#'. $_->invnum.
1128 ' ('. time2str('%x',$_->_date). ')',
1129 #'pkgpart' => 'N/A',
1131 'amount' => sprintf("%10.2f", $_->owed),
1137 # 'description' => 'Previous Balance',
1138 # #'pkgpart' => 'N/A',
1139 # 'pkgnum' => 'N/A',
1140 # 'amount' => sprintf("%10.2f", $pr_total ),
1141 # 'ext_description' => [ map {
1142 # "Invoice ". $_->invnum.
1143 # " (". time2str("%x",$_->_date). ") ".
1144 # sprintf("%10.2f", $_->owed)
1145 # } @pr_cust_bill ],
1152 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1153 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1158 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1159 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1162 sub _items_cust_bill_pkg {
1164 my $cust_bill_pkg = shift;
1167 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1169 if ( $cust_bill_pkg->pkgnum ) {
1171 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1172 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1173 my $pkg = $part_pkg->pkg;
1175 if ( $cust_bill_pkg->setup != 0 ) {
1176 my $description = $pkg;
1177 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1179 @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1181 'description' => $description,
1182 #'pkgpart' => $part_pkg->pkgpart,
1183 'pkgnum' => $cust_pkg->pkgnum,
1184 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1185 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1186 $cust_pkg->labels ),
1192 if ( $cust_bill_pkg->recur != 0 ) {
1194 'description' => "$pkg (" .
1195 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1196 time2str('%x', $cust_bill_pkg->edate). ')',
1197 #'pkgpart' => $part_pkg->pkgpart,
1198 'pkgnum' => $cust_pkg->pkgnum,
1199 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1200 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1201 $cust_pkg->labels ),
1202 $cust_bill_pkg->details,
1207 } else { #pkgnum tax or one-shot line item (??)
1209 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1210 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1212 if ( $cust_bill_pkg->setup != 0 ) {
1214 'description' => $itemdesc,
1215 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1218 if ( $cust_bill_pkg->recur != 0 ) {
1220 'description' => "$itemdesc (".
1221 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1222 time2str("%x", $cust_bill_pkg->edate). ')',
1223 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1235 sub _items_credits {
1240 foreach ( $self->cust_credited ) {
1242 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1244 my $reason = $_->cust_credit->reason;
1245 #my $reason = substr($_->cust_credit->reason,0,32);
1246 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1247 $reason = " ($reason) " if $reason;
1249 #'description' => 'Credit ref\#'. $_->crednum.
1250 # " (". time2str("%x",$_->cust_credit->_date) .")".
1252 'description' => 'Credit applied'.
1253 time2str("%x",$_->cust_credit->_date). $reason,
1254 'amount' => sprintf("%10.2f",$_->amount),
1257 #foreach ( @cr_cust_credit ) {
1259 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1260 # $money_char. sprintf("%10.2f",$_->credited)
1268 sub _items_payments {
1272 #get & print payments
1273 foreach ( $self->cust_bill_pay ) {
1275 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1278 'description' => "Payment received ".
1279 time2str("%x",$_->cust_pay->_date ),
1280 'amount' => sprintf("%10.2f", $_->amount )
1294 print_text formatting (and some logic :/) is in source, but needs to be
1295 slurped in from a file. Also number of lines ($=).
1297 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1298 or something similar so the look can be completely customized?)
1302 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1303 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base