4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
9 use FS::UID qw( datasrc );
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
13 use FS::cust_bill_pkg;
17 use FS::cust_credit_bill;
18 use FS::cust_pay_batch;
19 use FS::cust_bill_event;
21 @ISA = qw( FS::Record );
23 #ask FS::UID to run this stuff for us later
24 FS::UID->install_callback( sub {
26 $money_char = $conf->config('money_char') || '$';
31 FS::cust_bill - Object methods for cust_bill records
37 $record = new FS::cust_bill \%hash;
38 $record = new FS::cust_bill { 'column' => 'value' };
40 $error = $record->insert;
42 $error = $new_record->replace($old_record);
44 $error = $record->delete;
46 $error = $record->check;
48 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
50 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
52 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
54 @cust_pay_objects = $cust_bill->cust_pay;
56 $tax_amount = $record->tax;
58 @lines = $cust_bill->print_text;
59 @lines = $cust_bill->print_text $time;
63 An FS::cust_bill object represents an invoice; a declaration that a customer
64 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
65 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
66 following fields are currently supported:
70 =item invnum - primary key (assigned automatically for new invoices)
72 =item custnum - customer (see L<FS::cust_main>)
74 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
75 L<Time::Local> and L<Date::Parse> for conversion functions.
77 =item charged - amount of this invoice
79 =item printed - deprecated
81 =item closed - books closed flag, empty or `Y'
91 Creates a new invoice. To add the invoice to the database, see L<"insert">.
92 Invoices are normally created by calling the bill method of a customer object
93 (see L<FS::cust_main>).
97 sub table { 'cust_bill'; }
101 Adds this invoice to the database ("Posts" the invoice). If there is an error,
102 returns the error, otherwise returns false.
106 Currently unimplemented. I don't remove invoices because there would then be
107 no record you ever posted this invoice (which is bad, no?)
113 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
114 $self->SUPER::delete(@_);
117 =item replace OLD_RECORD
119 Replaces the OLD_RECORD with this one in the database. If there is an error,
120 returns the error, otherwise returns false.
122 Only printed may be changed. printed is normally updated by calling the
123 collect method of a customer object (see L<FS::cust_main>).
128 my( $new, $old ) = ( shift, shift );
129 return "Can't change custnum!" unless $old->custnum == $new->custnum;
130 #return "Can't change _date!" unless $old->_date eq $new->_date;
131 return "Can't change _date!" unless $old->_date == $new->_date;
132 return "Can't change charged!" unless $old->charged == $new->charged;
134 $new->SUPER::replace($old);
139 Checks all fields to make sure this is a valid invoice. If there is an error,
140 returns the error, otherwise returns false. Called by the insert and replace
149 $self->ut_numbern('invnum')
150 || $self->ut_number('custnum')
151 || $self->ut_numbern('_date')
152 || $self->ut_money('charged')
153 || $self->ut_numbern('printed')
154 || $self->ut_enum('closed', [ '', 'Y' ])
156 return $error if $error;
158 return "Unknown customer"
159 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
161 $self->_date(time) unless $self->_date;
163 $self->printed(0) if $self->printed eq '';
170 Returns a list consisting of the total previous balance for this customer,
171 followed by the previous outstanding invoices (as FS::cust_bill objects also).
178 my @cust_bill = sort { $a->_date <=> $b->_date }
179 grep { $_->owed != 0 && $_->_date < $self->_date }
180 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
182 foreach ( @cust_bill ) { $total += $_->owed; }
188 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
194 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
197 =item cust_bill_event
199 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
204 sub cust_bill_event {
206 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
212 Returns the customer (see L<FS::cust_main>) for this invoice.
218 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
223 Depreciated. See the cust_credited method.
225 #Returns a list consisting of the total previous credited (see
226 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
227 #outstanding credits (FS::cust_credit objects).
233 croak "FS::cust_bill->cust_credit depreciated; see ".
234 "FS::cust_bill->cust_credit_bill";
237 #my @cust_credit = sort { $a->_date <=> $b->_date }
238 # grep { $_->credited != 0 && $_->_date < $self->_date }
239 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
241 #foreach (@cust_credit) { $total += $_->credited; }
242 #$total, @cust_credit;
247 Depreciated. See the cust_bill_pay method.
249 #Returns all payments (see L<FS::cust_pay>) for this invoice.
255 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
257 #sort { $a->_date <=> $b->_date }
258 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
264 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
270 sort { $a->_date <=> $b->_date }
271 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
276 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
282 sort { $a->_date <=> $b->_date }
283 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
289 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
296 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
298 foreach (@taxlines) { $total += $_->setup; }
304 Returns the amount owed (still outstanding) on this invoice, which is charged
305 minus all payment applications (see L<FS::cust_bill_pay>) and credit
306 applications (see L<FS::cust_credit_bill>).
312 my $balance = $self->charged;
313 $balance -= $_->amount foreach ( $self->cust_bill_pay );
314 $balance -= $_->amount foreach ( $self->cust_credited );
315 $balance = sprintf( "%.2f", $balance);
316 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
320 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
322 Sends this invoice to the destinations configured for this customer: send
323 emails or print. See L<FS::cust_main_invoice>.
325 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
327 AGENTNUM, if specified, means that this invoice will only be sent for customers
328 of the specified agent.
330 INVOICE_FROM, if specified, overrides the default email invoice From: address.
336 my $template = scalar(@_) ? shift : '';
337 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
341 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
343 my @print_text = $self->print_text('', $template);
344 my @invoicing_list = $self->cust_main->invoicing_list;
346 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
348 #better to notify this person than silence
349 @invoicing_list = ($invoice_from) unless @invoicing_list;
351 my $error = send_email(
352 'from' => $invoice_from,
353 'to' => [ grep { $_ ne 'POST' } @invoicing_list ],
354 'subject' => 'Invoice',
355 'body' => \@print_text,
357 die "can't email invoice: $error\n" if $error;
361 if ( $conf->config('invoice_latex') ) {
362 @print_text = $self->print_ps('', $template);
365 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
366 my $lpr = $conf->config('lpr');
368 or die "Can't open pipe to $lpr: $!\n";
369 print LPR @print_text;
371 or die $! ? "Error closing $lpr: $!\n"
372 : "Exit status $? from $lpr\n";
379 =item send_csv OPTIONS
381 Sends invoice as a CSV data-file to a remote host with the specified protocol.
385 protocol - currently only "ftp"
391 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
392 and YYMMDDHHMMSS is a timestamp.
394 The fields of the CSV file is as follows:
396 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
400 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
402 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
403 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
404 fields are filled in.
406 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
407 first two fields (B<record_type> and B<invnum>) and the last five fields
408 (B<pkg> through B<edate>) are filled in.
410 =item invnum - invoice number
412 =item custnum - customer number
414 =item _date - invoice date
416 =item charged - total invoice amount
418 =item first - customer first name
420 =item last - customer first name
422 =item company - company name
424 =item address1 - address line 1
426 =item address2 - address line 1
436 =item pkg - line item description
438 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
440 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
442 =item sdate - start date for recurring fee
444 =item edate - end date for recurring fee
451 my($self, %opt) = @_;
453 #part one: create file
455 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
456 mkdir $spooldir, 0700 unless -d $spooldir;
458 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
460 open(CSV, ">$file") or die "can't open $file: $!";
462 eval "use Text::CSV_XS";
465 my $csv = Text::CSV_XS->new({'always_quote'=>1});
467 my $cust_main = $self->cust_main;
473 time2str("%x", $self->_date),
474 sprintf("%.2f", $self->charged),
475 ( map { $cust_main->getfield($_) }
476 qw( first last company address1 address2 city state zip country ) ),
478 ) or die "can't create csv";
479 print CSV $csv->string. "\n";
481 #new charges (false laziness w/print_text)
482 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
484 my($pkg, $setup, $recur, $sdate, $edate);
485 if ( $cust_bill_pkg->pkgnum ) {
487 ($pkg, $setup, $recur, $sdate, $edate) = (
488 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
489 ( $cust_bill_pkg->setup != 0
490 ? sprintf("%.2f", $cust_bill_pkg->setup )
492 ( $cust_bill_pkg->recur != 0
493 ? sprintf("%.2f", $cust_bill_pkg->recur )
495 time2str("%x", $cust_bill_pkg->sdate),
496 time2str("%x", $cust_bill_pkg->edate),
500 next unless $cust_bill_pkg->setup != 0;
501 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
502 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
504 ($pkg, $setup, $recur, $sdate, $edate) =
505 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
511 ( map { '' } (1..11) ),
512 ($pkg, $setup, $recur, $sdate, $edate)
513 ) or die "can't create csv";
514 print CSV $csv->string. "\n";
518 close CSV or die "can't close CSV: $!";
523 if ( $opt{protocol} eq 'ftp' ) {
524 eval "use Net::FTP;";
526 $net = Net::FTP->new($opt{server}) or die @$;
528 die "unknown protocol: $opt{protocol}";
531 $net->login( $opt{username}, $opt{password} )
532 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
534 $net->binary or die "can't set binary mode";
536 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
538 $net->put($file) or die "can't put $file: $!";
548 Pays this invoice with a compliemntary payment. If there is an error,
549 returns the error, otherwise returns false.
555 my $cust_pay = new FS::cust_pay ( {
556 'invnum' => $self->invnum,
557 'paid' => $self->owed,
560 'payinfo' => $self->cust_main->payinfo,
568 Attempts to pay this invoice with a credit card payment via a
569 Business::OnlinePayment realtime gateway. See
570 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
571 for supported processors.
577 $self->realtime_bop( 'CC', @_ );
582 Attempts to pay this invoice with an electronic check (ACH) payment via a
583 Business::OnlinePayment realtime gateway. See
584 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
585 for supported processors.
591 $self->realtime_bop( 'ECHECK', @_ );
596 Attempts to pay this invoice with phone bill (LEC) payment via a
597 Business::OnlinePayment realtime gateway. See
598 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
599 for supported processors.
605 $self->realtime_bop( 'LEC', @_ );
609 my( $self, $method ) = @_;
611 my $cust_main = $self->cust_main;
612 my $balance = $cust_main->balance;
613 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
614 $amount = sprintf("%.2f", $amount);
615 return "not run (balance $balance)" unless $amount > 0;
617 my $description = 'Internet Services';
618 if ( $conf->exists('business-onlinepayment-description') ) {
619 my $dtempl = $conf->config('business-onlinepayment-description');
621 my $agent_obj = $cust_main->agent
622 or die "can't retreive agent for $cust_main (agentnum ".
623 $cust_main->agentnum. ")";
624 my $agent = $agent_obj->agent;
625 my $pkgs = join(', ',
626 map { $_->cust_pkg->part_pkg->pkg }
627 grep { $_->pkgnum } $self->cust_bill_pkg
629 $description = eval qq("$dtempl");
632 $cust_main->realtime_bop($method, $amount,
633 'description' => $description,
634 'invnum' => $self->invnum,
641 Adds a payment for this invoice to the pending credit card batch (see
642 L<FS::cust_pay_batch>).
648 my $cust_main = $self->cust_main;
650 my $cust_pay_batch = new FS::cust_pay_batch ( {
651 'invnum' => $self->getfield('invnum'),
652 'custnum' => $cust_main->getfield('custnum'),
653 'last' => $cust_main->getfield('last'),
654 'first' => $cust_main->getfield('first'),
655 'address1' => $cust_main->getfield('address1'),
656 'address2' => $cust_main->getfield('address2'),
657 'city' => $cust_main->getfield('city'),
658 'state' => $cust_main->getfield('state'),
659 'zip' => $cust_main->getfield('zip'),
660 'country' => $cust_main->getfield('country'),
661 'cardnum' => $cust_main->getfield('payinfo'),
662 'exp' => $cust_main->getfield('paydate'),
663 'payname' => $cust_main->getfield('payname'),
664 'amount' => $self->owed,
666 my $error = $cust_pay_batch->insert;
667 die $error if $error;
672 sub _agent_template {
674 $self->_agent_plandata('agent_templatename');
677 sub _agent_invoice_from {
679 $self->_agent_plandata('agent_invoice_from');
682 sub _agent_plandata {
683 my( $self, $option ) = @_;
685 my $cust_bill_event = qsearchs( 'part_bill_event',
687 'payby' => $self->cust_main->payby,
688 'plan' => 'send_agent',
689 'plandata' => { 'op' => '~',
690 'value' => "(^|\n)agentnum ".
691 $self->cust_main->agentnum.
696 'ORDER BY seconds LIMIT 1'
699 return '' unless $cust_bill_event;
701 if ( $cust_bill_event->plandata =~ /^$option (.*)$/m ) {
704 warn "can't parse plandata for $1";
710 =item print_text [ TIME [ , TEMPLATE ] ]
712 Returns an text invoice, as a list of lines.
714 TIME an optional value used to control the printing of overdue messages. The
715 default is now. It isn't the date of the invoice; that's the `_date' field.
716 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
717 L<Time::Local> and L<Date::Parse> for conversion functions.
723 my( $self, $today, $template ) = @_;
725 # my $invnum = $self->invnum;
726 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
727 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
728 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
730 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
731 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
732 #my $balance_due = $self->owed + $pr_total - $cr_total;
733 my $balance_due = $self->owed + $pr_total;
736 #my($description,$amount);
740 foreach ( @pr_cust_bill ) {
742 "Previous Balance, Invoice #". $_->invnum.
743 " (". time2str("%x",$_->_date). ")",
744 $money_char. sprintf("%10.2f",$_->owed)
748 push @buf,['','-----------'];
749 push @buf,[ 'Total Previous Balance',
750 $money_char. sprintf("%10.2f",$pr_total ) ];
755 foreach my $cust_bill_pkg (
756 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
757 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
760 if ( $cust_bill_pkg->pkgnum ) {
762 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
763 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
764 my $pkg = $part_pkg->pkg;
766 if ( $cust_bill_pkg->setup != 0 ) {
767 my $description = $pkg;
768 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
769 push @buf, [ $description,
770 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
772 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
775 if ( $cust_bill_pkg->recur != 0 ) {
777 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
778 time2str("%x", $cust_bill_pkg->edate) . ")",
779 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
782 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
785 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
787 } else { #pkgnum tax or one-shot line item
788 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
789 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
791 if ( $cust_bill_pkg->setup != 0 ) {
792 push @buf, [ $itemdesc,
793 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
795 if ( $cust_bill_pkg->recur != 0 ) {
796 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
797 . time2str("%x", $cust_bill_pkg->edate). ")",
798 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
804 push @buf,['','-----------'];
805 push @buf,['Total New Charges',
806 $money_char. sprintf("%10.2f",$self->charged) ];
809 push @buf,['','-----------'];
810 push @buf,['Total Charges',
811 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
815 foreach ( $self->cust_credited ) {
817 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
819 my $reason = substr($_->cust_credit->reason,0,32);
820 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
821 $reason = " ($reason) " if $reason;
823 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
825 $money_char. sprintf("%10.2f",$_->amount)
828 #foreach ( @cr_cust_credit ) {
830 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
831 # $money_char. sprintf("%10.2f",$_->credited)
835 #get & print payments
836 foreach ( $self->cust_bill_pay ) {
838 #something more elaborate if $_->amount ne ->cust_pay->paid ?
841 "Payment received ". time2str("%x",$_->cust_pay->_date ),
842 $money_char. sprintf("%10.2f",$_->amount )
847 my $balance_due_msg = $self->balance_due_msg;
849 push @buf,['','-----------'];
850 push @buf,[$balance_due_msg, $money_char.
851 sprintf("%10.2f", $balance_due ) ];
854 $template ||= $self->_agent_template;
855 my $templatefile = 'invoice_template';
856 $templatefile .= "_$template" if length($template);
857 my @invoice_template = $conf->config($templatefile)
858 or die "cannot load config file $templatefile";
861 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
862 /invoice_lines\((\d*)\)/;
863 $invoice_lines += $1 || scalar(@buf);
866 die "no invoice_lines() functions in template?" unless $wasfunc;
867 my $invoice_template = new Text::Template (
869 SOURCE => [ map "$_\n", @invoice_template ],
870 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
871 $invoice_template->compile()
872 or die "can't compile template: $Text::Template::ERROR";
874 #setup template variables
875 package FS::cust_bill::_template; #!
876 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
878 $invnum = $self->invnum;
879 $date = $self->_date;
881 $agent = $self->cust_main->agent->agent;
883 if ( $FS::cust_bill::invoice_lines ) {
885 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
887 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
892 #format address (variable for the template)
894 @address = ( '', '', '', '', '', '' );
895 package FS::cust_bill; #!
896 $FS::cust_bill::_template::address[$l++] =
898 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
899 ? " (P.O. #". $cust_main->payinfo. ")"
903 $FS::cust_bill::_template::address[$l++] = $cust_main->company
904 if $cust_main->company;
905 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
906 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
907 if $cust_main->address2;
908 $FS::cust_bill::_template::address[$l++] =
909 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
910 $FS::cust_bill::_template::address[$l++] = $cust_main->country
911 unless $cust_main->country eq 'US';
913 # #overdue? (variable for the template)
914 # $FS::cust_bill::_template::overdue = (
916 # && $today > $self->_date
917 ## && $self->printed > 1
918 # && $self->printed > 0
921 #and subroutine for the template
922 sub FS::cust_bill::_template::invoice_lines {
923 my $lines = shift || scalar(@buf);
925 scalar(@buf) ? shift @buf : [ '', '' ];
931 $FS::cust_bill::_template::page = 1;
935 push @collect, split("\n",
936 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
938 $FS::cust_bill::_template::page++;
941 map "$_\n", @collect;
945 =item print_latex [ TIME [ , TEMPLATE ] ]
947 Internal method - returns a filename of a filled-in LaTeX template for this
948 invoice (Note: add ".tex" to get the actual filename).
950 See print_ps and print_pdf for methods that return PostScript and PDF output.
952 TIME an optional value used to control the printing of overdue messages. The
953 default is now. It isn't the date of the invoice; that's the `_date' field.
954 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
955 L<Time::Local> and L<Date::Parse> for conversion functions.
959 #still some false laziness w/print_text
962 my( $self, $today, $template ) = @_;
965 # my $invnum = $self->invnum;
966 my $cust_main = $self->cust_main;
967 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
968 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
970 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
971 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
972 #my $balance_due = $self->owed + $pr_total - $cr_total;
973 my $balance_due = $self->owed + $pr_total;
976 #my($description,$amount);
980 $template ||= $self->_agent_template;
981 my $templatefile = 'invoice_latex';
982 my $suffix = length($template) ? "_$template" : '';
983 $templatefile .= $suffix;
984 my @invoice_template = $conf->config($templatefile)
985 or die "cannot load config file $templatefile";
988 'invnum' => $self->invnum,
989 'date' => time2str('%b %o, %Y', $self->_date),
990 'agent' => _latex_escape($cust_main->agent->agent),
991 'payname' => _latex_escape($cust_main->payname),
992 'company' => _latex_escape($cust_main->company),
993 'address1' => _latex_escape($cust_main->address1),
994 'address2' => _latex_escape($cust_main->address2),
995 'city' => _latex_escape($cust_main->city),
996 'state' => _latex_escape($cust_main->state),
997 'zip' => _latex_escape($cust_main->zip),
998 'country' => _latex_escape($cust_main->country),
999 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1000 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1002 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1003 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1006 my $countrydefault = $conf->config('countrydefault') || 'US';
1007 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1009 #do variable substitutions in notes
1010 $invoice_data{'notes'} =
1012 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1013 $conf->config_orbase('invoice_latexnotes', $suffix)
1016 $invoice_data{'footer'} =~ s/\n+$//;
1017 $invoice_data{'smallfooter'} =~ s/\n+$//;
1018 $invoice_data{'notes'} =~ s/\n+$//;
1020 $invoice_data{'po_line'} =
1021 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1022 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1026 my @total_item = ();
1028 while ( @invoice_template ) {
1029 my $line = shift @invoice_template;
1031 if ( $line =~ /^%%Detail\s*$/ ) {
1033 while ( ( my $line_item_line = shift @invoice_template )
1034 !~ /^%%EndDetail\s*$/ ) {
1035 push @line_item, $line_item_line;
1037 foreach my $line_item ( $self->_items ) {
1038 #foreach my $line_item ( $self->_items_pkg ) {
1039 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1040 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1041 if ( exists $line_item->{'ext_description'} ) {
1042 $invoice_data{'description'} .=
1043 "\\tabularnewline\n~~".
1044 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1046 $invoice_data{'amount'} = $line_item->{'amount'};
1047 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1049 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1052 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1054 while ( ( my $total_item_line = shift @invoice_template )
1055 !~ /^%%EndTotalDetails\s*$/ ) {
1056 push @total_item, $total_item_line;
1059 my @total_fill = ();
1062 foreach my $tax ( $self->_items_tax ) {
1063 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1064 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1066 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1071 $invoice_data{'total_item'} = 'Sub-total';
1072 $invoice_data{'total_amount'} =
1073 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1074 unshift @total_fill,
1075 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1079 $invoice_data{'total_item'} = '\textbf{Total}';
1080 $invoice_data{'total_amount'} =
1081 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1083 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1086 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1089 foreach my $credit ( $self->_items_credits ) {
1090 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1092 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1094 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1099 foreach my $payment ( $self->_items_payments ) {
1100 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1102 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1104 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1108 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1109 $invoice_data{'total_amount'} =
1110 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1112 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1115 push @filled_in, @total_fill;
1118 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1119 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1120 push @filled_in, $line;
1131 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1132 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1136 ) or die "can't open temp file: $!\n";
1137 print $fh join("\n", @filled_in ), "\n";
1140 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1145 =item print_ps [ TIME [ , TEMPLATE ] ]
1147 Returns an postscript invoice, as a scalar.
1149 TIME an optional value used to control the printing of overdue messages. The
1150 default is now. It isn't the date of the invoice; that's the `_date' field.
1151 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1152 L<Time::Local> and L<Date::Parse> for conversion functions.
1159 my $file = $self->print_latex(@_);
1161 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1164 system("pslatex $file.tex >/dev/null 2>&1") == 0
1165 or die "pslatex $file.tex failed: $!";
1166 system("pslatex $file.tex >/dev/null 2>&1") == 0
1167 or die "pslatex $file.tex failed: $!";
1169 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1170 or die "dvips failed: $!";
1172 open(POSTSCRIPT, "<$file.ps")
1173 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1175 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1178 while (<POSTSCRIPT>) {
1188 =item print_pdf [ TIME [ , TEMPLATE ] ]
1190 Returns an PDF invoice, as a scalar.
1192 TIME an optional value used to control the printing of overdue messages. The
1193 default is now. It isn't the date of the invoice; that's the `_date' field.
1194 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1195 L<Time::Local> and L<Date::Parse> for conversion functions.
1202 my $file = $self->print_latex(@_);
1204 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1207 #system('pdflatex', "$file.tex");
1208 #system('pdflatex', "$file.tex");
1209 #! LaTeX Error: Unknown graphics extension: .eps.
1211 system("pslatex $file.tex >/dev/null 2>&1") == 0
1212 or die "pslatex $file.tex failed: $!";
1213 system("pslatex $file.tex >/dev/null 2>&1") == 0
1214 or die "pslatex $file.tex failed: $!";
1216 #system('dvipdf', "$file.dvi", "$file.pdf" );
1218 "dvips -q -t letter -f $file.dvi ".
1219 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
1222 or die "dvips | gs failed: $!";
1224 open(PDF, "<$file.pdf")
1225 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1227 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1240 # quick subroutine for print_latex
1242 # There are ten characters that LaTeX treats as special characters, which
1243 # means that they do not simply typeset themselves:
1244 # # $ % & ~ _ ^ \ { }
1246 # TeX ignores blanks following an escaped character; if you want a blank (as
1247 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1251 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1255 #utility methods for print_*
1257 sub balance_due_msg {
1259 my $msg = 'Balance Due';
1260 return $msg unless $conf->exists('invoice_default_terms');
1261 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1262 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1263 } elsif ( $conf->config('invoice_default_terms') ) {
1264 $msg .= ' - '. $conf->config('invoice_default_terms');
1271 my @display = scalar(@_)
1273 : qw( _items_previous _items_pkg );
1274 #: qw( _items_pkg );
1275 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1277 foreach my $display ( @display ) {
1278 push @b, $self->$display(@_);
1283 sub _items_previous {
1285 my $cust_main = $self->cust_main;
1286 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1288 foreach ( @pr_cust_bill ) {
1290 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1291 ' ('. time2str('%x',$_->_date). ')',
1292 #'pkgpart' => 'N/A',
1294 'amount' => sprintf("%10.2f", $_->owed),
1300 # 'description' => 'Previous Balance',
1301 # #'pkgpart' => 'N/A',
1302 # 'pkgnum' => 'N/A',
1303 # 'amount' => sprintf("%10.2f", $pr_total ),
1304 # 'ext_description' => [ map {
1305 # "Invoice ". $_->invnum.
1306 # " (". time2str("%x",$_->_date). ") ".
1307 # sprintf("%10.2f", $_->owed)
1308 # } @pr_cust_bill ],
1315 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1316 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1321 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1322 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1325 sub _items_cust_bill_pkg {
1327 my $cust_bill_pkg = shift;
1330 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1332 if ( $cust_bill_pkg->pkgnum ) {
1334 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1335 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1336 my $pkg = $part_pkg->pkg;
1339 #tie %labels, 'Tie::IxHash';
1340 push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1341 my @ext_description;
1342 foreach my $label ( keys %labels ) {
1343 my @values = @{ $labels{$label} };
1344 my $num = scalar(@values);
1346 push @ext_description, "$label ($num)";
1348 push @ext_description, map { "$label: $_" } @values;
1352 if ( $cust_bill_pkg->setup != 0 ) {
1353 my $description = $pkg;
1354 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1355 my @d = @ext_description;
1356 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1358 'description' => $description,
1359 #'pkgpart' => $part_pkg->pkgpart,
1360 'pkgnum' => $cust_pkg->pkgnum,
1361 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1362 'ext_description' => \@d,
1366 if ( $cust_bill_pkg->recur != 0 ) {
1368 'description' => "$pkg (" .
1369 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1370 time2str('%x', $cust_bill_pkg->edate). ')',
1371 #'pkgpart' => $part_pkg->pkgpart,
1372 'pkgnum' => $cust_pkg->pkgnum,
1373 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1374 'ext_description' => [ @ext_description,
1375 $cust_bill_pkg->details,
1380 } else { #pkgnum tax or one-shot line item (??)
1382 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1383 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1385 if ( $cust_bill_pkg->setup != 0 ) {
1387 'description' => $itemdesc,
1388 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1391 if ( $cust_bill_pkg->recur != 0 ) {
1393 'description' => "$itemdesc (".
1394 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1395 time2str("%x", $cust_bill_pkg->edate). ')',
1396 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1408 sub _items_credits {
1413 foreach ( $self->cust_credited ) {
1415 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1417 my $reason = $_->cust_credit->reason;
1418 #my $reason = substr($_->cust_credit->reason,0,32);
1419 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1420 $reason = " ($reason) " if $reason;
1422 #'description' => 'Credit ref\#'. $_->crednum.
1423 # " (". time2str("%x",$_->cust_credit->_date) .")".
1425 'description' => 'Credit applied'.
1426 time2str("%x",$_->cust_credit->_date). $reason,
1427 'amount' => sprintf("%10.2f",$_->amount),
1430 #foreach ( @cr_cust_credit ) {
1432 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1433 # $money_char. sprintf("%10.2f",$_->credited)
1441 sub _items_payments {
1445 #get & print payments
1446 foreach ( $self->cust_bill_pay ) {
1448 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1451 'description' => "Payment received ".
1452 time2str("%x",$_->cust_pay->_date ),
1453 'amount' => sprintf("%10.2f", $_->amount )
1467 print_text formatting (and some logic :/) is in source, but needs to be
1468 slurped in from a file. Also number of lines ($=).
1472 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1473 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base