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') ),
1140 'returnaddress' => join("\n", $conf->config('invoice_latexreturnaddress') ),
1142 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1143 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1144 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1147 my $countrydefault = $conf->config('countrydefault') || 'US';
1148 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1150 #do variable substitutions in notes
1151 $invoice_data{'notes'} =
1153 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1154 $conf->config_orbase('invoice_latexnotes', $suffix)
1157 $invoice_data{'footer'} =~ s/\n+$//;
1158 $invoice_data{'smallfooter'} =~ s/\n+$//;
1159 $invoice_data{'notes'} =~ s/\n+$//;
1161 $invoice_data{'po_line'} =
1162 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1163 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1167 if ( $format eq 'old' ) {
1170 my @total_item = ();
1171 while ( @invoice_template ) {
1172 my $line = shift @invoice_template;
1174 if ( $line =~ /^%%Detail\s*$/ ) {
1176 while ( ( my $line_item_line = shift @invoice_template )
1177 !~ /^%%EndDetail\s*$/ ) {
1178 push @line_item, $line_item_line;
1180 foreach my $line_item ( $self->_items ) {
1181 #foreach my $line_item ( $self->_items_pkg ) {
1182 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1183 $invoice_data{'description'} =
1184 _latex_escape($line_item->{'description'});
1185 if ( exists $line_item->{'ext_description'} ) {
1186 $invoice_data{'description'} .=
1187 "\\tabularnewline\n~~".
1188 join( "\\tabularnewline\n~~",
1189 map _latex_escape($_), @{$line_item->{'ext_description'}}
1192 $invoice_data{'amount'} = $line_item->{'amount'};
1193 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1195 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1198 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1200 while ( ( my $total_item_line = shift @invoice_template )
1201 !~ /^%%EndTotalDetails\s*$/ ) {
1202 push @total_item, $total_item_line;
1205 my @total_fill = ();
1208 foreach my $tax ( $self->_items_tax ) {
1209 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1210 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1212 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1217 $invoice_data{'total_item'} = 'Sub-total';
1218 $invoice_data{'total_amount'} =
1219 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1220 unshift @total_fill,
1221 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1225 $invoice_data{'total_item'} = '\textbf{Total}';
1226 $invoice_data{'total_amount'} =
1227 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1229 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1232 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1235 foreach my $credit ( $self->_items_credits ) {
1236 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1238 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1240 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1245 foreach my $payment ( $self->_items_payments ) {
1246 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1248 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1250 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1254 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1255 $invoice_data{'total_amount'} =
1256 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1258 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1261 push @filled_in, @total_fill;
1264 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1265 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1266 push @filled_in, $line;
1277 } elsif ( $format eq 'Text::Template' ) {
1279 my @detail_items = ();
1280 my @total_items = ();
1282 $invoice_data{'detail_items'} = \@detail_items;
1283 $invoice_data{'total_items'} = \@total_items;
1285 foreach my $line_item ( $self->_items ) {
1287 ext_description => [],
1289 $detail->{'ref'} = $line_item->{'pkgnum'};
1290 $detail->{'quantity'} = 1;
1291 $detail->{'description'} = _latex_escape($line_item->{'description'});
1292 if ( exists $line_item->{'ext_description'} ) {
1293 @{$detail->{'ext_description'}} = map {
1295 } @{$line_item->{'ext_description'}};
1297 $detail->{'amount'} = $line_item->{'amount'};
1298 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1300 push @detail_items, $detail;
1305 foreach my $tax ( $self->_items_tax ) {
1307 $total->{'total_item'} = _latex_escape($tax->{'description'});
1308 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1309 push @total_items, $total;
1314 $total->{'total_item'} = 'Sub-total';
1315 $total->{'total_amount'} =
1316 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1317 unshift @total_items, $total;
1322 $total->{'total_item'} = '\textbf{Total}';
1323 $total->{'total_amount'} =
1324 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1325 push @total_items, $total;
1328 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1331 foreach my $credit ( $self->_items_credits ) {
1333 $total->{'total_item'} = _latex_escape($credit->{'description'});
1335 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1336 push @total_items, $total;
1340 foreach my $payment ( $self->_items_payments ) {
1342 $total->{'total_item'} = _latex_escape($payment->{'description'});
1344 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1345 push @total_items, $total;
1350 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1351 $total->{'total_amount'} =
1352 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1353 push @total_items, $total;
1357 die "guru meditation #54";
1360 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1361 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1365 ) or die "can't open temp file: $!\n";
1366 if ( $format eq 'old' ) {
1367 print $fh join('', @filled_in );
1368 } elsif ( $format eq 'Text::Template' ) {
1369 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1371 die "guru meditation #32";
1375 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1380 =item print_ps [ TIME [ , TEMPLATE ] ]
1382 Returns an postscript invoice, as a scalar.
1384 TIME an optional value used to control the printing of overdue messages. The
1385 default is now. It isn't the date of the invoice; that's the `_date' field.
1386 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1387 L<Time::Local> and L<Date::Parse> for conversion functions.
1394 my $file = $self->print_latex(@_);
1396 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1399 my $sfile = shell_quote $file;
1401 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1402 or die "pslatex $file.tex failed; see $file.log for details?\n";
1403 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1404 or die "pslatex $file.tex failed; see $file.log for details?\n";
1406 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1407 or die "dvips failed";
1409 open(POSTSCRIPT, "<$file.ps")
1410 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1412 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1415 while (<POSTSCRIPT>) {
1425 =item print_pdf [ TIME [ , TEMPLATE ] ]
1427 Returns an PDF invoice, as a scalar.
1429 TIME an optional value used to control the printing of overdue messages. The
1430 default is now. It isn't the date of the invoice; that's the `_date' field.
1431 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1432 L<Time::Local> and L<Date::Parse> for conversion functions.
1439 my $file = $self->print_latex(@_);
1441 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1444 #system('pdflatex', "$file.tex");
1445 #system('pdflatex', "$file.tex");
1446 #! LaTeX Error: Unknown graphics extension: .eps.
1448 my $sfile = shell_quote $file;
1450 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1451 or die "pslatex $file.tex failed; see $file.log for details?\n";
1452 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1453 or die "pslatex $file.tex failed; see $file.log for details?\n";
1455 #system('dvipdf', "$file.dvi", "$file.pdf" );
1457 "dvips -q -t letter -f $sfile.dvi ".
1458 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1461 or die "dvips | gs failed: $!";
1463 open(PDF, "<$file.pdf")
1464 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1466 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1479 # quick subroutine for print_latex
1481 # There are ten characters that LaTeX treats as special characters, which
1482 # means that they do not simply typeset themselves:
1483 # # $ % & ~ _ ^ \ { }
1485 # TeX ignores blanks following an escaped character; if you want a blank (as
1486 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1490 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1491 $value =~ s/([<>])/\$$1\$/g;
1495 #utility methods for print_*
1497 sub balance_due_msg {
1499 my $msg = 'Balance Due';
1500 return $msg unless $conf->exists('invoice_default_terms');
1501 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1502 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1503 } elsif ( $conf->config('invoice_default_terms') ) {
1504 $msg .= ' - '. $conf->config('invoice_default_terms');
1511 my @display = scalar(@_)
1513 : qw( _items_previous _items_pkg );
1514 #: qw( _items_pkg );
1515 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1517 foreach my $display ( @display ) {
1518 push @b, $self->$display(@_);
1523 sub _items_previous {
1525 my $cust_main = $self->cust_main;
1526 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1528 foreach ( @pr_cust_bill ) {
1530 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1531 ' ('. time2str('%x',$_->_date). ')',
1532 #'pkgpart' => 'N/A',
1534 'amount' => sprintf("%10.2f", $_->owed),
1540 # 'description' => 'Previous Balance',
1541 # #'pkgpart' => 'N/A',
1542 # 'pkgnum' => 'N/A',
1543 # 'amount' => sprintf("%10.2f", $pr_total ),
1544 # 'ext_description' => [ map {
1545 # "Invoice ". $_->invnum.
1546 # " (". time2str("%x",$_->_date). ") ".
1547 # sprintf("%10.2f", $_->owed)
1548 # } @pr_cust_bill ],
1555 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1556 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1561 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1562 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1565 sub _items_cust_bill_pkg {
1567 my $cust_bill_pkg = shift;
1570 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1572 if ( $cust_bill_pkg->pkgnum ) {
1574 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1575 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1576 my $pkg = $part_pkg->pkg;
1578 if ( $cust_bill_pkg->setup != 0 ) {
1579 my $description = $pkg;
1580 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1581 my @d = $cust_pkg->h_labels_short($self->_date);
1582 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1584 description => $description,
1585 #pkgpart => $part_pkg->pkgpart,
1586 pkgnum => $cust_pkg->pkgnum,
1587 amount => sprintf("%10.2f", $cust_bill_pkg->setup),
1588 ext_description => \@d,
1592 if ( $cust_bill_pkg->recur != 0 ) {
1594 description => "$pkg (" .
1595 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1596 time2str('%x', $cust_bill_pkg->edate). ')',
1597 #pkgpart => $part_pkg->pkgpart,
1598 pkgnum => $cust_pkg->pkgnum,
1599 amount => sprintf("%10.2f", $cust_bill_pkg->recur),
1600 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1601 $cust_bill_pkg->sdate),
1602 $cust_bill_pkg->details,
1607 } else { #pkgnum tax or one-shot line item (??)
1609 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1610 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1612 if ( $cust_bill_pkg->setup != 0 ) {
1614 'description' => $itemdesc,
1615 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1618 if ( $cust_bill_pkg->recur != 0 ) {
1620 'description' => "$itemdesc (".
1621 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1622 time2str("%x", $cust_bill_pkg->edate). ')',
1623 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1635 sub _items_credits {
1640 foreach ( $self->cust_credited ) {
1642 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1644 my $reason = $_->cust_credit->reason;
1645 #my $reason = substr($_->cust_credit->reason,0,32);
1646 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1647 $reason = " ($reason) " if $reason;
1649 #'description' => 'Credit ref\#'. $_->crednum.
1650 # " (". time2str("%x",$_->cust_credit->_date) .")".
1652 'description' => 'Credit applied '.
1653 time2str("%x",$_->cust_credit->_date). $reason,
1654 'amount' => sprintf("%10.2f",$_->amount),
1657 #foreach ( @cr_cust_credit ) {
1659 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1660 # $money_char. sprintf("%10.2f",$_->credited)
1668 sub _items_payments {
1672 #get & print payments
1673 foreach ( $self->cust_bill_pay ) {
1675 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1678 'description' => "Payment received ".
1679 time2str("%x",$_->cust_pay->_date ),
1680 'amount' => sprintf("%10.2f", $_->amount )
1694 print_text formatting (and some logic :/) is in source, but needs to be
1695 slurped in from a file. Also number of lines ($=).
1699 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1700 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base