4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
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 ) { #email
423 #better to notify this person than silence
424 @invoicing_list = ($invoice_from) unless @invoicing_list;
426 my $error = send_email(
427 $self->generate_email(
428 'from' => $invoice_from,
429 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
430 'print_text' => [ @print_text ],
433 die "can't email invoice: $error\n" if $error;
434 #die "$error\n" if $error;
438 if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
440 if ($conf->config('invoice_latex')) {
441 $lpr_data = [ $self->print_ps('', $template) ];
443 $lpr_data = \@print_text;
446 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
447 my $lpr = $conf->config('lpr');
449 or die "Can't open pipe to $lpr: $!\n";
450 print LPR @{$lpr_data};
452 or die $! ? "Error closing $lpr: $!\n"
453 : "Exit status $? from $lpr\n";
456 if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
457 unless ($conf->exists('invoice_latex')) {
458 die 'FAX invoice destination not supported with plain text invoices.'
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 = $conf->config($templatefile)
1106 or die "cannot load config file $templatefile";
1108 my %invoice_data = (
1109 'invnum' => $self->invnum,
1110 'date' => time2str('%b %o, %Y', $self->_date),
1111 'agent' => _latex_escape($cust_main->agent->agent),
1112 'payname' => _latex_escape($cust_main->payname),
1113 'company' => _latex_escape($cust_main->company),
1114 'address1' => _latex_escape($cust_main->address1),
1115 'address2' => _latex_escape($cust_main->address2),
1116 'city' => _latex_escape($cust_main->city),
1117 'state' => _latex_escape($cust_main->state),
1118 'zip' => _latex_escape($cust_main->zip),
1119 'country' => _latex_escape($cust_main->country),
1120 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1121 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1123 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1124 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1127 my $countrydefault = $conf->config('countrydefault') || 'US';
1128 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1130 #do variable substitutions in notes
1131 $invoice_data{'notes'} =
1133 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1134 $conf->config_orbase('invoice_latexnotes', $suffix)
1137 $invoice_data{'footer'} =~ s/\n+$//;
1138 $invoice_data{'smallfooter'} =~ s/\n+$//;
1139 $invoice_data{'notes'} =~ s/\n+$//;
1141 $invoice_data{'po_line'} =
1142 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1143 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1147 my @total_item = ();
1149 while ( @invoice_template ) {
1150 my $line = shift @invoice_template;
1152 if ( $line =~ /^%%Detail\s*$/ ) {
1154 while ( ( my $line_item_line = shift @invoice_template )
1155 !~ /^%%EndDetail\s*$/ ) {
1156 push @line_item, $line_item_line;
1158 foreach my $line_item ( $self->_items ) {
1159 #foreach my $line_item ( $self->_items_pkg ) {
1160 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1161 $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1162 if ( exists $line_item->{'ext_description'} ) {
1163 $invoice_data{'description'} .=
1164 "\\tabularnewline\n~~".
1165 join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1167 $invoice_data{'amount'} = $line_item->{'amount'};
1168 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1170 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1173 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1175 while ( ( my $total_item_line = shift @invoice_template )
1176 !~ /^%%EndTotalDetails\s*$/ ) {
1177 push @total_item, $total_item_line;
1180 my @total_fill = ();
1183 foreach my $tax ( $self->_items_tax ) {
1184 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1185 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1187 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1192 $invoice_data{'total_item'} = 'Sub-total';
1193 $invoice_data{'total_amount'} =
1194 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1195 unshift @total_fill,
1196 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1200 $invoice_data{'total_item'} = '\textbf{Total}';
1201 $invoice_data{'total_amount'} =
1202 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1204 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1207 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1210 foreach my $credit ( $self->_items_credits ) {
1211 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1213 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1215 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1220 foreach my $payment ( $self->_items_payments ) {
1221 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1223 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1225 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1229 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1230 $invoice_data{'total_amount'} =
1231 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1233 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1236 push @filled_in, @total_fill;
1239 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1240 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1241 push @filled_in, $line;
1252 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1253 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1257 ) or die "can't open temp file: $!\n";
1258 print $fh join("\n", @filled_in ), "\n";
1261 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1266 =item print_ps [ TIME [ , TEMPLATE ] ]
1268 Returns an postscript invoice, as a scalar.
1270 TIME an optional value used to control the printing of overdue messages. The
1271 default is now. It isn't the date of the invoice; that's the `_date' field.
1272 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1273 L<Time::Local> and L<Date::Parse> for conversion functions.
1280 my $file = $self->print_latex(@_);
1282 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1285 my $sfile = shell_quote $file;
1287 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1288 or die "pslatex $file.tex failed; see $file.log for details?\n";
1289 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1290 or die "pslatex $file.tex failed; see $file.log for details?\n";
1292 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1293 or die "dvips failed";
1295 open(POSTSCRIPT, "<$file.ps")
1296 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1298 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1301 while (<POSTSCRIPT>) {
1311 =item print_pdf [ TIME [ , TEMPLATE ] ]
1313 Returns an PDF invoice, as a scalar.
1315 TIME an optional value used to control the printing of overdue messages. The
1316 default is now. It isn't the date of the invoice; that's the `_date' field.
1317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1318 L<Time::Local> and L<Date::Parse> for conversion functions.
1325 my $file = $self->print_latex(@_);
1327 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1330 #system('pdflatex', "$file.tex");
1331 #system('pdflatex', "$file.tex");
1332 #! LaTeX Error: Unknown graphics extension: .eps.
1334 my $sfile = shell_quote $file;
1336 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1337 or die "pslatex $file.tex failed: $!";
1338 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1339 or die "pslatex $file.tex failed: $!";
1341 #system('dvipdf', "$file.dvi", "$file.pdf" );
1343 "dvips -q -t letter -f $sfile.dvi ".
1344 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1347 or die "dvips | gs failed: $!";
1349 open(PDF, "<$file.pdf")
1350 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1352 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1365 # quick subroutine for print_latex
1367 # There are ten characters that LaTeX treats as special characters, which
1368 # means that they do not simply typeset themselves:
1369 # # $ % & ~ _ ^ \ { }
1371 # TeX ignores blanks following an escaped character; if you want a blank (as
1372 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1376 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1380 #utility methods for print_*
1382 sub balance_due_msg {
1384 my $msg = 'Balance Due';
1385 return $msg unless $conf->exists('invoice_default_terms');
1386 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1387 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1388 } elsif ( $conf->config('invoice_default_terms') ) {
1389 $msg .= ' - '. $conf->config('invoice_default_terms');
1396 my @display = scalar(@_)
1398 : qw( _items_previous _items_pkg );
1399 #: qw( _items_pkg );
1400 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1402 foreach my $display ( @display ) {
1403 push @b, $self->$display(@_);
1408 sub _items_previous {
1410 my $cust_main = $self->cust_main;
1411 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1413 foreach ( @pr_cust_bill ) {
1415 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1416 ' ('. time2str('%x',$_->_date). ')',
1417 #'pkgpart' => 'N/A',
1419 'amount' => sprintf("%10.2f", $_->owed),
1425 # 'description' => 'Previous Balance',
1426 # #'pkgpart' => 'N/A',
1427 # 'pkgnum' => 'N/A',
1428 # 'amount' => sprintf("%10.2f", $pr_total ),
1429 # 'ext_description' => [ map {
1430 # "Invoice ". $_->invnum.
1431 # " (". time2str("%x",$_->_date). ") ".
1432 # sprintf("%10.2f", $_->owed)
1433 # } @pr_cust_bill ],
1440 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1441 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1446 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1447 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1450 sub _items_cust_bill_pkg {
1452 my $cust_bill_pkg = shift;
1455 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1457 if ( $cust_bill_pkg->pkgnum ) {
1459 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1460 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1461 my $pkg = $part_pkg->pkg;
1463 if ( $cust_bill_pkg->setup != 0 ) {
1464 my $description = $pkg;
1465 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1466 my @d = $cust_pkg->h_labels_short($self->_date);
1467 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1469 description => $description,
1470 #pkgpart => $part_pkg->pkgpart,
1471 pkgnum => $cust_pkg->pkgnum,
1472 amount => sprintf("%10.2f", $cust_bill_pkg->setup),
1473 ext_description => \@d,
1477 if ( $cust_bill_pkg->recur != 0 ) {
1479 description => "$pkg (" .
1480 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1481 time2str('%x', $cust_bill_pkg->edate). ')',
1482 #pkgpart => $part_pkg->pkgpart,
1483 pkgnum => $cust_pkg->pkgnum,
1484 amount => sprintf("%10.2f", $cust_bill_pkg->recur),
1485 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1486 $cust_bill_pkg->sdate),
1487 $cust_bill_pkg->details,
1492 } else { #pkgnum tax or one-shot line item (??)
1494 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1495 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1497 if ( $cust_bill_pkg->setup != 0 ) {
1499 'description' => $itemdesc,
1500 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1503 if ( $cust_bill_pkg->recur != 0 ) {
1505 'description' => "$itemdesc (".
1506 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1507 time2str("%x", $cust_bill_pkg->edate). ')',
1508 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1520 sub _items_credits {
1525 foreach ( $self->cust_credited ) {
1527 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1529 my $reason = $_->cust_credit->reason;
1530 #my $reason = substr($_->cust_credit->reason,0,32);
1531 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1532 $reason = " ($reason) " if $reason;
1534 #'description' => 'Credit ref\#'. $_->crednum.
1535 # " (". time2str("%x",$_->cust_credit->_date) .")".
1537 'description' => 'Credit applied '.
1538 time2str("%x",$_->cust_credit->_date). $reason,
1539 'amount' => sprintf("%10.2f",$_->amount),
1542 #foreach ( @cr_cust_credit ) {
1544 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1545 # $money_char. sprintf("%10.2f",$_->credited)
1553 sub _items_payments {
1557 #get & print payments
1558 foreach ( $self->cust_bill_pay ) {
1560 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1563 'description' => "Payment received ".
1564 time2str("%x",$_->cust_pay->_date ),
1565 'amount' => sprintf("%10.2f", $_->amount )
1579 print_text formatting (and some logic :/) is in source, but needs to be
1580 slurped in from a file. Also number of lines ($=).
1584 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1585 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base