4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
7 use Text::Template 1.20;
9 use String::ShellQuote;
10 use FS::UID qw( datasrc );
11 use FS::Record qw( qsearch qsearchs );
12 use FS::Misc qw( send_email send_fax );
14 use FS::cust_bill_pkg;
18 use FS::cust_credit_bill;
19 use FS::cust_pay_batch;
20 use FS::cust_bill_event;
22 @ISA = qw( FS::Record );
24 #ask FS::UID to run this stuff for us later
25 FS::UID->install_callback( sub {
27 $money_char = $conf->config('money_char') || '$';
32 FS::cust_bill - Object methods for cust_bill records
38 $record = new FS::cust_bill \%hash;
39 $record = new FS::cust_bill { 'column' => 'value' };
41 $error = $record->insert;
43 $error = $new_record->replace($old_record);
45 $error = $record->delete;
47 $error = $record->check;
49 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
51 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
53 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
55 @cust_pay_objects = $cust_bill->cust_pay;
57 $tax_amount = $record->tax;
59 @lines = $cust_bill->print_text;
60 @lines = $cust_bill->print_text $time;
64 An FS::cust_bill object represents an invoice; a declaration that a customer
65 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
66 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
67 following fields are currently supported:
71 =item invnum - primary key (assigned automatically for new invoices)
73 =item custnum - customer (see L<FS::cust_main>)
75 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
76 L<Time::Local> and L<Date::Parse> for conversion functions.
78 =item charged - amount of this invoice
80 =item printed - deprecated
82 =item closed - books closed flag, empty or `Y'
92 Creates a new invoice. To add the invoice to the database, see L<"insert">.
93 Invoices are normally created by calling the bill method of a customer object
94 (see L<FS::cust_main>).
98 sub table { 'cust_bill'; }
102 Adds this invoice to the database ("Posts" the invoice). If there is an error,
103 returns the error, otherwise returns false.
107 Currently unimplemented. I don't remove invoices because there would then be
108 no record you ever posted this invoice (which is bad, no?)
114 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
115 $self->SUPER::delete(@_);
118 =item replace OLD_RECORD
120 Replaces the OLD_RECORD with this one in the database. If there is an error,
121 returns the error, otherwise returns false.
123 Only printed may be changed. printed is normally updated by calling the
124 collect method of a customer object (see L<FS::cust_main>).
129 my( $new, $old ) = ( shift, shift );
130 return "Can't change custnum!" unless $old->custnum == $new->custnum;
131 #return "Can't change _date!" unless $old->_date eq $new->_date;
132 return "Can't change _date!" unless $old->_date == $new->_date;
133 return "Can't change charged!" unless $old->charged == $new->charged;
135 $new->SUPER::replace($old);
140 Checks all fields to make sure this is a valid invoice. If there is an error,
141 returns the error, otherwise returns false. Called by the insert and replace
150 $self->ut_numbern('invnum')
151 || $self->ut_number('custnum')
152 || $self->ut_numbern('_date')
153 || $self->ut_money('charged')
154 || $self->ut_numbern('printed')
155 || $self->ut_enum('closed', [ '', 'Y' ])
157 return $error if $error;
159 return "Unknown customer"
160 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
162 $self->_date(time) unless $self->_date;
164 $self->printed(0) if $self->printed eq '';
171 Returns a list consisting of the total previous balance for this customer,
172 followed by the previous outstanding invoices (as FS::cust_bill objects also).
179 my @cust_bill = sort { $a->_date <=> $b->_date }
180 grep { $_->owed != 0 && $_->_date < $self->_date }
181 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
183 foreach ( @cust_bill ) { $total += $_->owed; }
189 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
195 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
198 =item cust_bill_event
200 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
205 sub cust_bill_event {
207 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
213 Returns the customer (see L<FS::cust_main>) for this invoice.
219 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
224 Depreciated. See the cust_credited method.
226 #Returns a list consisting of the total previous credited (see
227 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
228 #outstanding credits (FS::cust_credit objects).
234 croak "FS::cust_bill->cust_credit depreciated; see ".
235 "FS::cust_bill->cust_credit_bill";
238 #my @cust_credit = sort { $a->_date <=> $b->_date }
239 # grep { $_->credited != 0 && $_->_date < $self->_date }
240 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
242 #foreach (@cust_credit) { $total += $_->credited; }
243 #$total, @cust_credit;
248 Depreciated. See the cust_bill_pay method.
250 #Returns all payments (see L<FS::cust_pay>) for this invoice.
256 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
258 #sort { $a->_date <=> $b->_date }
259 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
265 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
271 sort { $a->_date <=> $b->_date }
272 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
277 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
283 sort { $a->_date <=> $b->_date }
284 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
290 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
297 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
299 foreach (@taxlines) { $total += $_->setup; }
305 Returns the amount owed (still outstanding) on this invoice, which is charged
306 minus all payment applications (see L<FS::cust_bill_pay>) and credit
307 applications (see L<FS::cust_credit_bill>).
313 my $balance = $self->charged;
314 $balance -= $_->amount foreach ( $self->cust_bill_pay );
315 $balance -= $_->amount foreach ( $self->cust_credited );
316 $balance = sprintf( "%.2f", $balance);
317 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
322 =item generate_email PARAMHASH
324 PARAMHASH can contain the following:
328 =item from => sender address, required
330 =item tempate => alternate template name, optional
332 =item print_text => text attachment arrayref, optional
334 =item subject => email subject, optional
338 Returns an argument list to be passed to L<FS::Misc::send_email>.
348 if ($conf->exists('invoice_email_pdf')) {
349 #warn "[FS::cust_bill::send] creating PDF attachment";
350 #mime parts arguments a la MIME::Entity->build().
353 'Type' => 'application/pdf',
354 'Encoding' => 'base64',
355 'Data' => [ $self->print_pdf('', $args{'template'}) ],
356 'Disposition' => 'attachment',
357 'Filename' => 'invoice.pdf',
363 if ($conf->exists('invoice_email_pdf')
364 and scalar($conf->config('invoice_email_pdf_note'))) {
366 #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'";
367 $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ];
369 #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'";
370 if (ref($args{'print_text'}) eq 'ARRAY') {
371 $email_text = $args{'print_text'};
373 $email_text = [ $self->print_text('', $args{'template'}) ];
378 if (ref($args{'to'} eq 'ARRAY')) {
379 @invoicing_list = @{$args{'to'}};
381 @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
385 'from' => $args{'from'},
386 'to' => [ @invoicing_list ],
387 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
388 'body' => $email_text,
389 'mimeparts' => $mimeparts,
395 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
397 Sends this invoice to the destinations configured for this customer: send
398 emails or print. See L<FS::cust_main_invoice>.
400 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
402 AGENTNUM, if specified, means that this invoice will only be sent for customers
403 of the specified agent.
405 INVOICE_FROM, if specified, overrides the default email invoice From: address.
411 my $template = scalar(@_) ? shift : '';
412 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
416 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
418 my @print_text = $self->print_text('', $template);
419 my @invoicing_list = $self->cust_main->invoicing_list;
421 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) {
424 #better to notify this person than silence
425 @invoicing_list = ($invoice_from) unless @invoicing_list;
427 my $error = send_email(
428 $self->generate_email(
429 'from' => $invoice_from,
430 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
431 'print_text' => [ @print_text ],
434 die "can't email invoice: $error\n" if $error;
435 #die "$error\n" if $error;
439 if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
441 if ($conf->config('invoice_latex')) {
442 $lpr_data = [ $self->print_ps('', $template) ];
444 $lpr_data = \@print_text;
447 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
448 my $lpr = $conf->config('lpr');
450 or die "Can't open pipe to $lpr: $!\n";
451 print LPR @{$lpr_data};
453 or die $! ? "Error closing $lpr: $!\n"
454 : "Exit status $? from $lpr\n";
457 if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
458 die 'FAX invoice destination not supported with plain text invoices.'
459 unless $conf->exists('invoice_latex');
460 my $dialstring = $self->cust_main->getfield('fax');
462 my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
463 die $error if $error;
472 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
474 Like B<send>, but only sends the invoice if it is the newest open invoice for
484 grep { $_->owed > 0 }
485 qsearch('cust_bill', {
486 'custnum' => $self->custnum,
487 #'_date' => { op=>'>', value=>$self->_date },
488 'invnum' => { op=>'>', value=>$self->invnum },
495 =item send_csv OPTIONS
497 Sends invoice as a CSV data-file to a remote host with the specified protocol.
501 protocol - currently only "ftp"
507 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
508 and YYMMDDHHMMSS is a timestamp.
510 The fields of the CSV file is as follows:
512 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
516 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
518 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
519 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
520 fields are filled in.
522 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
523 first two fields (B<record_type> and B<invnum>) and the last five fields
524 (B<pkg> through B<edate>) are filled in.
526 =item invnum - invoice number
528 =item custnum - customer number
530 =item _date - invoice date
532 =item charged - total invoice amount
534 =item first - customer first name
536 =item last - customer first name
538 =item company - company name
540 =item address1 - address line 1
542 =item address2 - address line 1
552 =item pkg - line item description
554 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
556 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
558 =item sdate - start date for recurring fee
560 =item edate - end date for recurring fee
567 my($self, %opt) = @_;
569 #part one: create file
571 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
572 mkdir $spooldir, 0700 unless -d $spooldir;
574 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
576 open(CSV, ">$file") or die "can't open $file: $!";
578 eval "use Text::CSV_XS";
581 my $csv = Text::CSV_XS->new({'always_quote'=>1});
583 my $cust_main = $self->cust_main;
589 time2str("%x", $self->_date),
590 sprintf("%.2f", $self->charged),
591 ( map { $cust_main->getfield($_) }
592 qw( first last company address1 address2 city state zip country ) ),
594 ) or die "can't create csv";
595 print CSV $csv->string. "\n";
597 #new charges (false laziness w/print_text)
598 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
600 my($pkg, $setup, $recur, $sdate, $edate);
601 if ( $cust_bill_pkg->pkgnum ) {
603 ($pkg, $setup, $recur, $sdate, $edate) = (
604 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
605 ( $cust_bill_pkg->setup != 0
606 ? sprintf("%.2f", $cust_bill_pkg->setup )
608 ( $cust_bill_pkg->recur != 0
609 ? sprintf("%.2f", $cust_bill_pkg->recur )
611 time2str("%x", $cust_bill_pkg->sdate),
612 time2str("%x", $cust_bill_pkg->edate),
616 next unless $cust_bill_pkg->setup != 0;
617 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
618 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
620 ($pkg, $setup, $recur, $sdate, $edate) =
621 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
627 ( map { '' } (1..11) ),
628 ($pkg, $setup, $recur, $sdate, $edate)
629 ) or die "can't create csv";
630 print CSV $csv->string. "\n";
634 close CSV or die "can't close CSV: $!";
639 if ( $opt{protocol} eq 'ftp' ) {
640 eval "use Net::FTP;";
642 $net = Net::FTP->new($opt{server}) or die @$;
644 die "unknown protocol: $opt{protocol}";
647 $net->login( $opt{username}, $opt{password} )
648 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
650 $net->binary or die "can't set binary mode";
652 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
654 $net->put($file) or die "can't put $file: $!";
664 Pays this invoice with a compliemntary payment. If there is an error,
665 returns the error, otherwise returns false.
671 my $cust_pay = new FS::cust_pay ( {
672 'invnum' => $self->invnum,
673 'paid' => $self->owed,
676 'payinfo' => $self->cust_main->payinfo,
684 Attempts to pay this invoice with a credit card payment via a
685 Business::OnlinePayment realtime gateway. See
686 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
687 for supported processors.
693 $self->realtime_bop( 'CC', @_ );
698 Attempts to pay this invoice with an electronic check (ACH) payment via a
699 Business::OnlinePayment realtime gateway. See
700 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
701 for supported processors.
707 $self->realtime_bop( 'ECHECK', @_ );
712 Attempts to pay this invoice with phone bill (LEC) payment via a
713 Business::OnlinePayment realtime gateway. See
714 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
715 for supported processors.
721 $self->realtime_bop( 'LEC', @_ );
725 my( $self, $method ) = @_;
727 my $cust_main = $self->cust_main;
728 my $balance = $cust_main->balance;
729 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
730 $amount = sprintf("%.2f", $amount);
731 return "not run (balance $balance)" unless $amount > 0;
733 my $description = 'Internet Services';
734 if ( $conf->exists('business-onlinepayment-description') ) {
735 my $dtempl = $conf->config('business-onlinepayment-description');
737 my $agent_obj = $cust_main->agent
738 or die "can't retreive agent for $cust_main (agentnum ".
739 $cust_main->agentnum. ")";
740 my $agent = $agent_obj->agent;
741 my $pkgs = join(', ',
742 map { $_->cust_pkg->part_pkg->pkg }
743 grep { $_->pkgnum } $self->cust_bill_pkg
745 $description = eval qq("$dtempl");
748 $cust_main->realtime_bop($method, $amount,
749 'description' => $description,
750 'invnum' => $self->invnum,
757 Adds a payment for this invoice to the pending credit card batch (see
758 L<FS::cust_pay_batch>).
764 my $cust_main = $self->cust_main;
766 my $cust_pay_batch = new FS::cust_pay_batch ( {
767 'invnum' => $self->getfield('invnum'),
768 'custnum' => $cust_main->getfield('custnum'),
769 'last' => $cust_main->getfield('last'),
770 'first' => $cust_main->getfield('first'),
771 'address1' => $cust_main->getfield('address1'),
772 'address2' => $cust_main->getfield('address2'),
773 'city' => $cust_main->getfield('city'),
774 'state' => $cust_main->getfield('state'),
775 'zip' => $cust_main->getfield('zip'),
776 'country' => $cust_main->getfield('country'),
777 'cardnum' => $cust_main->payinfo,
778 'exp' => $cust_main->getfield('paydate'),
779 'payname' => $cust_main->getfield('payname'),
780 'amount' => $self->owed,
782 my $error = $cust_pay_batch->insert;
783 die $error if $error;
788 sub _agent_template {
790 $self->_agent_plandata('agent_templatename');
793 sub _agent_invoice_from {
795 $self->_agent_plandata('agent_invoice_from');
798 sub _agent_plandata {
799 my( $self, $option ) = @_;
801 my $part_bill_event = qsearchs( 'part_bill_event',
803 'payby' => $self->cust_main->payby,
804 'plan' => 'send_agent',
805 'plandata' => { 'op' => '~',
806 'value' => "(^|\n)agentnum ".
807 $self->cust_main->agentnum.
812 'ORDER BY seconds LIMIT 1'
815 return '' unless $part_bill_event;
817 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
820 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
821 " plandata for $option";
827 =item print_text [ TIME [ , TEMPLATE ] ]
829 Returns an text invoice, as a list of lines.
831 TIME an optional value used to control the printing of overdue messages. The
832 default is now. It isn't the date of the invoice; that's the `_date' field.
833 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
834 L<Time::Local> and L<Date::Parse> for conversion functions.
838 #still some false laziness w/print_text
841 my( $self, $today, $template ) = @_;
844 # my $invnum = $self->invnum;
845 my $cust_main = $self->cust_main;
846 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
847 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
849 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
850 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
851 #my $balance_due = $self->owed + $pr_total - $cr_total;
852 my $balance_due = $self->owed + $pr_total;
855 #my($description,$amount);
859 foreach ( @pr_cust_bill ) {
861 "Previous Balance, Invoice #". $_->invnum.
862 " (". time2str("%x",$_->_date). ")",
863 $money_char. sprintf("%10.2f",$_->owed)
867 push @buf,['','-----------'];
868 push @buf,[ 'Total Previous Balance',
869 $money_char. sprintf("%10.2f",$pr_total ) ];
874 foreach my $cust_bill_pkg (
875 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
876 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
879 if ( $cust_bill_pkg->pkgnum ) {
881 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
882 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
883 my $pkg = $part_pkg->pkg;
885 if ( $cust_bill_pkg->setup != 0 ) {
886 my $description = $pkg;
887 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
888 push @buf, [ $description,
889 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
891 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
892 $cust_pkg->h_labels($self->_date);
895 if ( $cust_bill_pkg->recur != 0 ) {
897 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
898 time2str("%x", $cust_bill_pkg->edate) . ")",
899 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
902 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
903 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
906 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
908 } else { #pkgnum tax or one-shot line item
909 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
910 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
912 if ( $cust_bill_pkg->setup != 0 ) {
913 push @buf, [ $itemdesc,
914 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
916 if ( $cust_bill_pkg->recur != 0 ) {
917 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
918 . time2str("%x", $cust_bill_pkg->edate). ")",
919 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
925 push @buf,['','-----------'];
926 push @buf,['Total New Charges',
927 $money_char. sprintf("%10.2f",$self->charged) ];
930 push @buf,['','-----------'];
931 push @buf,['Total Charges',
932 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
936 foreach ( $self->cust_credited ) {
938 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
940 my $reason = substr($_->cust_credit->reason,0,32);
941 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
942 $reason = " ($reason) " if $reason;
944 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
946 $money_char. sprintf("%10.2f",$_->amount)
949 #foreach ( @cr_cust_credit ) {
951 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
952 # $money_char. sprintf("%10.2f",$_->credited)
956 #get & print payments
957 foreach ( $self->cust_bill_pay ) {
959 #something more elaborate if $_->amount ne ->cust_pay->paid ?
962 "Payment received ". time2str("%x",$_->cust_pay->_date ),
963 $money_char. sprintf("%10.2f",$_->amount )
968 my $balance_due_msg = $self->balance_due_msg;
970 push @buf,['','-----------'];
971 push @buf,[$balance_due_msg, $money_char.
972 sprintf("%10.2f", $balance_due ) ];
975 $template ||= $self->_agent_template;
976 my $templatefile = 'invoice_template';
977 $templatefile .= "_$template" if length($template);
978 my @invoice_template = $conf->config($templatefile)
979 or die "cannot load config file $templatefile";
982 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
983 /invoice_lines\((\d*)\)/;
984 $invoice_lines += $1 || scalar(@buf);
987 die "no invoice_lines() functions in template?" unless $wasfunc;
988 my $invoice_template = new Text::Template (
990 SOURCE => [ map "$_\n", @invoice_template ],
991 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
992 $invoice_template->compile()
993 or die "can't compile template: $Text::Template::ERROR";
995 #setup template variables
996 package FS::cust_bill::_template; #!
997 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
999 $invnum = $self->invnum;
1000 $date = $self->_date;
1002 $agent = $self->cust_main->agent->agent;
1004 if ( $FS::cust_bill::invoice_lines ) {
1006 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1008 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1013 #format address (variable for the template)
1015 @address = ( '', '', '', '', '', '' );
1016 package FS::cust_bill; #!
1017 $FS::cust_bill::_template::address[$l++] =
1018 $cust_main->payname.
1019 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1020 ? " (P.O. #". $cust_main->payinfo. ")"
1024 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1025 if $cust_main->company;
1026 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1027 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1028 if $cust_main->address2;
1029 $FS::cust_bill::_template::address[$l++] =
1030 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1031 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1032 unless $cust_main->country eq 'US';
1034 # #overdue? (variable for the template)
1035 # $FS::cust_bill::_template::overdue = (
1037 # && $today > $self->_date
1038 ## && $self->printed > 1
1039 # && $self->printed > 0
1042 #and subroutine for the template
1043 sub FS::cust_bill::_template::invoice_lines {
1044 my $lines = shift || scalar(@buf);
1046 scalar(@buf) ? shift @buf : [ '', '' ];
1052 $FS::cust_bill::_template::page = 1;
1056 push @collect, split("\n",
1057 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1059 $FS::cust_bill::_template::page++;
1062 map "$_\n", @collect;
1066 =item print_latex [ TIME [ , TEMPLATE ] ]
1068 Internal method - returns a filename of a filled-in LaTeX template for this
1069 invoice (Note: add ".tex" to get the actual filename).
1071 See print_ps and print_pdf for methods that return PostScript and PDF output.
1073 TIME an optional value used to control the printing of overdue messages. The
1074 default is now. It isn't the date of the invoice; that's the `_date' field.
1075 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1076 L<Time::Local> and L<Date::Parse> for conversion functions.
1080 #still some false laziness w/print_text
1083 my( $self, $today, $template ) = @_;
1086 # my $invnum = $self->invnum;
1087 my $cust_main = $self->cust_main;
1088 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1089 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1091 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1092 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1093 #my $balance_due = $self->owed + $pr_total - $cr_total;
1094 my $balance_due = $self->owed + $pr_total;
1097 #my($description,$amount);
1100 #create the template
1101 $template ||= $self->_agent_template;
1102 my $templatefile = 'invoice_latex';
1103 my $suffix = length($template) ? "_$template" : '';
1104 $templatefile .= $suffix;
1105 my @invoice_template = map "$_\n", $conf->config($templatefile)
1106 or die "cannot load config file $templatefile";
1108 my($format, $text_template);
1109 if ( grep { /^%%Detail/ } @invoice_template ) {
1110 #change this to a die when the old code is removed
1111 warn "old-style invoice template $templatefile; ".
1112 "patch with conf/invoice_latex.diff\n";
1115 $format = 'Text::Template';
1116 $text_template = new Text::Template(
1118 SOURCE => \@invoice_template,
1119 DELIMITERS => [ '[@--', '--@]' ],
1122 $text_template->compile()
1123 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1126 my %invoice_data = (
1127 'invnum' => $self->invnum,
1128 'date' => time2str('%b %o, %Y', $self->_date),
1129 'agent' => _latex_escape($cust_main->agent->agent),
1130 'payname' => _latex_escape($cust_main->payname),
1131 'company' => _latex_escape($cust_main->company),
1132 'address1' => _latex_escape($cust_main->address1),
1133 'address2' => _latex_escape($cust_main->address2),
1134 'city' => _latex_escape($cust_main->city),
1135 'state' => _latex_escape($cust_main->state),
1136 'zip' => _latex_escape($cust_main->zip),
1137 'country' => _latex_escape($cust_main->country),
1138 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1139 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1141 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1142 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1145 my $countrydefault = $conf->config('countrydefault') || 'US';
1146 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1148 #do variable substitutions in notes
1149 $invoice_data{'notes'} =
1151 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1152 $conf->config_orbase('invoice_latexnotes', $suffix)
1155 $invoice_data{'footer'} =~ s/\n+$//;
1156 $invoice_data{'smallfooter'} =~ s/\n+$//;
1157 $invoice_data{'notes'} =~ s/\n+$//;
1159 $invoice_data{'po_line'} =
1160 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1161 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1165 if ( $format eq 'old' ) {
1168 my @total_item = ();
1169 while ( @invoice_template ) {
1170 my $line = shift @invoice_template;
1172 if ( $line =~ /^%%Detail\s*$/ ) {
1174 while ( ( my $line_item_line = shift @invoice_template )
1175 !~ /^%%EndDetail\s*$/ ) {
1176 push @line_item, $line_item_line;
1178 foreach my $line_item ( $self->_items ) {
1179 #foreach my $line_item ( $self->_items_pkg ) {
1180 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1181 $invoice_data{'description'} =
1182 _latex_escape($line_item->{'description'});
1183 if ( exists $line_item->{'ext_description'} ) {
1184 $invoice_data{'description'} .=
1185 "\\tabularnewline\n~~".
1186 join( "\\tabularnewline\n~~",
1187 map _latex_escape($_), @{$line_item->{'ext_description'}}
1190 $invoice_data{'amount'} = $line_item->{'amount'};
1191 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1193 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1196 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1198 while ( ( my $total_item_line = shift @invoice_template )
1199 !~ /^%%EndTotalDetails\s*$/ ) {
1200 push @total_item, $total_item_line;
1203 my @total_fill = ();
1206 foreach my $tax ( $self->_items_tax ) {
1207 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1208 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1210 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1215 $invoice_data{'total_item'} = 'Sub-total';
1216 $invoice_data{'total_amount'} =
1217 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1218 unshift @total_fill,
1219 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1223 $invoice_data{'total_item'} = '\textbf{Total}';
1224 $invoice_data{'total_amount'} =
1225 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1227 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1230 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1233 foreach my $credit ( $self->_items_credits ) {
1234 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1236 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1238 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1243 foreach my $payment ( $self->_items_payments ) {
1244 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1246 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1248 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1252 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1253 $invoice_data{'total_amount'} =
1254 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1256 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1259 push @filled_in, @total_fill;
1262 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1263 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1264 push @filled_in, $line;
1275 } elsif ( $format eq 'Text::Template' ) {
1277 my @detail_items = ();
1278 my @total_items = ();
1280 $invoice_data{'detail_items'} = \@detail_items;
1281 $invoice_data{'total_items'} = \@total_items;
1283 foreach my $line_item ( $self->_items ) {
1285 ext_description => [],
1287 $detail->{'ref'} = $line_item->{'pkgnum'};
1288 $detail->{'quantity'} = 1;
1289 $detail->{'description'} = _latex_escape($line_item->{'description'});
1290 if ( exists $line_item->{'ext_description'} ) {
1291 @{$detail->{'ext_description'}} = map {
1293 } @{$line_item->{'ext_description'}};
1295 $detail->{'amount'} = $line_item->{'amount'};
1296 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1298 push @detail_items, $detail;
1303 foreach my $tax ( $self->_items_tax ) {
1305 $total->{'total_item'} = _latex_escape($tax->{'description'});
1306 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1307 push @total_items, $total;
1312 $total->{'total_item'} = 'Sub-total';
1313 $total->{'total_amount'} =
1314 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1315 unshift @total_items, $total;
1320 $total->{'total_item'} = '\textbf{Total}';
1321 $total->{'total_amount'} =
1322 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1323 push @total_items, $total;
1326 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1329 foreach my $credit ( $self->_items_credits ) {
1331 $total->{'total_item'} = _latex_escape($credit->{'description'});
1333 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1334 push @total_items, $total;
1338 foreach my $payment ( $self->_items_payments ) {
1340 $total->{'total_item'} = _latex_escape($payment->{'description'});
1342 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1343 push @total_items, $total;
1348 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1349 $total->{'total_amount'} =
1350 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1351 push @total_items, $total;
1355 die "guru meditation #54";
1358 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1359 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1363 ) or die "can't open temp file: $!\n";
1364 if ( $format eq 'old' ) {
1365 print $fh join('', @filled_in );
1366 } elsif ( $format eq 'Text::Template' ) {
1367 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1369 die "guru meditation #32";
1373 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1378 =item print_ps [ TIME [ , TEMPLATE ] ]
1380 Returns an postscript invoice, as a scalar.
1382 TIME an optional value used to control the printing of overdue messages. The
1383 default is now. It isn't the date of the invoice; that's the `_date' field.
1384 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1385 L<Time::Local> and L<Date::Parse> for conversion functions.
1392 my $file = $self->print_latex(@_);
1394 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1397 my $sfile = shell_quote $file;
1399 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1400 or die "pslatex $file.tex failed; see $file.log for details?\n";
1401 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1402 or die "pslatex $file.tex failed; see $file.log for details?\n";
1404 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1405 or die "dvips failed";
1407 open(POSTSCRIPT, "<$file.ps")
1408 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1410 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1413 while (<POSTSCRIPT>) {
1423 =item print_pdf [ TIME [ , TEMPLATE ] ]
1425 Returns an PDF invoice, as a scalar.
1427 TIME an optional value used to control the printing of overdue messages. The
1428 default is now. It isn't the date of the invoice; that's the `_date' field.
1429 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1430 L<Time::Local> and L<Date::Parse> for conversion functions.
1437 my $file = $self->print_latex(@_);
1439 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1442 #system('pdflatex', "$file.tex");
1443 #system('pdflatex', "$file.tex");
1444 #! LaTeX Error: Unknown graphics extension: .eps.
1446 my $sfile = shell_quote $file;
1448 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1449 or die "pslatex $file.tex failed: $!";
1450 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1451 or die "pslatex $file.tex failed: $!";
1453 #system('dvipdf', "$file.dvi", "$file.pdf" );
1455 "dvips -q -t letter -f $sfile.dvi ".
1456 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1459 or die "dvips | gs failed: $!";
1461 open(PDF, "<$file.pdf")
1462 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1464 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1477 # quick subroutine for print_latex
1479 # There are ten characters that LaTeX treats as special characters, which
1480 # means that they do not simply typeset themselves:
1481 # # $ % & ~ _ ^ \ { }
1483 # TeX ignores blanks following an escaped character; if you want a blank (as
1484 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1488 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1492 #utility methods for print_*
1494 sub balance_due_msg {
1496 my $msg = 'Balance Due';
1497 return $msg unless $conf->exists('invoice_default_terms');
1498 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1499 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1500 } elsif ( $conf->config('invoice_default_terms') ) {
1501 $msg .= ' - '. $conf->config('invoice_default_terms');
1508 my @display = scalar(@_)
1510 : qw( _items_previous _items_pkg );
1511 #: qw( _items_pkg );
1512 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1514 foreach my $display ( @display ) {
1515 push @b, $self->$display(@_);
1520 sub _items_previous {
1522 my $cust_main = $self->cust_main;
1523 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1525 foreach ( @pr_cust_bill ) {
1527 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1528 ' ('. time2str('%x',$_->_date). ')',
1529 #'pkgpart' => 'N/A',
1531 'amount' => sprintf("%10.2f", $_->owed),
1537 # 'description' => 'Previous Balance',
1538 # #'pkgpart' => 'N/A',
1539 # 'pkgnum' => 'N/A',
1540 # 'amount' => sprintf("%10.2f", $pr_total ),
1541 # 'ext_description' => [ map {
1542 # "Invoice ". $_->invnum.
1543 # " (". time2str("%x",$_->_date). ") ".
1544 # sprintf("%10.2f", $_->owed)
1545 # } @pr_cust_bill ],
1552 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1553 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1558 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1559 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1562 sub _items_cust_bill_pkg {
1564 my $cust_bill_pkg = shift;
1567 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1569 if ( $cust_bill_pkg->pkgnum ) {
1571 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1572 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1573 my $pkg = $part_pkg->pkg;
1575 if ( $cust_bill_pkg->setup != 0 ) {
1576 my $description = $pkg;
1577 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1578 my @d = $cust_pkg->h_labels_short($self->_date);
1579 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1581 description => $description,
1582 #pkgpart => $part_pkg->pkgpart,
1583 pkgnum => $cust_pkg->pkgnum,
1584 amount => sprintf("%10.2f", $cust_bill_pkg->setup),
1585 ext_description => \@d,
1589 if ( $cust_bill_pkg->recur != 0 ) {
1591 description => "$pkg (" .
1592 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1593 time2str('%x', $cust_bill_pkg->edate). ')',
1594 #pkgpart => $part_pkg->pkgpart,
1595 pkgnum => $cust_pkg->pkgnum,
1596 amount => sprintf("%10.2f", $cust_bill_pkg->recur),
1597 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1598 $cust_bill_pkg->sdate),
1599 $cust_bill_pkg->details,
1604 } else { #pkgnum tax or one-shot line item (??)
1606 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1607 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1609 if ( $cust_bill_pkg->setup != 0 ) {
1611 'description' => $itemdesc,
1612 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1615 if ( $cust_bill_pkg->recur != 0 ) {
1617 'description' => "$itemdesc (".
1618 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1619 time2str("%x", $cust_bill_pkg->edate). ')',
1620 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1632 sub _items_credits {
1637 foreach ( $self->cust_credited ) {
1639 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1641 my $reason = $_->cust_credit->reason;
1642 #my $reason = substr($_->cust_credit->reason,0,32);
1643 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1644 $reason = " ($reason) " if $reason;
1646 #'description' => 'Credit ref\#'. $_->crednum.
1647 # " (". time2str("%x",$_->cust_credit->_date) .")".
1649 'description' => 'Credit applied '.
1650 time2str("%x",$_->cust_credit->_date). $reason,
1651 'amount' => sprintf("%10.2f",$_->amount),
1654 #foreach ( @cr_cust_credit ) {
1656 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1657 # $money_char. sprintf("%10.2f",$_->credited)
1665 sub _items_payments {
1669 #get & print payments
1670 foreach ( $self->cust_bill_pay ) {
1672 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1675 'description' => "Payment received ".
1676 time2str("%x",$_->cust_pay->_date ),
1677 'amount' => sprintf("%10.2f", $_->amount )
1691 print_text formatting (and some logic :/) is in source, but needs to be
1692 slurped in from a file. Also number of lines ($=).
1696 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1697 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base