4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
7 use Text::Template 1.20;
9 use String::ShellQuote;
12 use FS::UID qw( datasrc );
13 use FS::Record qw( qsearch qsearchs );
14 use FS::Misc qw( send_email send_fax );
16 use FS::cust_bill_pkg;
20 use FS::cust_credit_bill;
21 use FS::cust_pay_batch;
22 use FS::cust_bill_event;
24 @ISA = qw( FS::Record );
28 #ask FS::UID to run this stuff for us later
29 FS::UID->install_callback( sub {
31 $money_char = $conf->config('money_char') || '$';
36 FS::cust_bill - Object methods for cust_bill records
42 $record = new FS::cust_bill \%hash;
43 $record = new FS::cust_bill { 'column' => 'value' };
45 $error = $record->insert;
47 $error = $new_record->replace($old_record);
49 $error = $record->delete;
51 $error = $record->check;
53 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
55 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
57 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
59 @cust_pay_objects = $cust_bill->cust_pay;
61 $tax_amount = $record->tax;
63 @lines = $cust_bill->print_text;
64 @lines = $cust_bill->print_text $time;
68 An FS::cust_bill object represents an invoice; a declaration that a customer
69 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
70 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
71 following fields are currently supported:
75 =item invnum - primary key (assigned automatically for new invoices)
77 =item custnum - customer (see L<FS::cust_main>)
79 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
80 L<Time::Local> and L<Date::Parse> for conversion functions.
82 =item charged - amount of this invoice
84 =item printed - deprecated
86 =item closed - books closed flag, empty or `Y'
96 Creates a new invoice. To add the invoice to the database, see L<"insert">.
97 Invoices are normally created by calling the bill method of a customer object
98 (see L<FS::cust_main>).
102 sub table { 'cust_bill'; }
106 Adds this invoice to the database ("Posts" the invoice). If there is an error,
107 returns the error, otherwise returns false.
111 Currently unimplemented. I don't remove invoices because there would then be
112 no record you ever posted this invoice (which is bad, no?)
118 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
119 $self->SUPER::delete(@_);
122 =item replace OLD_RECORD
124 Replaces the OLD_RECORD with this one in the database. If there is an error,
125 returns the error, otherwise returns false.
127 Only printed may be changed. printed is normally updated by calling the
128 collect method of a customer object (see L<FS::cust_main>).
133 my( $new, $old ) = ( shift, shift );
134 return "Can't change custnum!" unless $old->custnum == $new->custnum;
135 #return "Can't change _date!" unless $old->_date eq $new->_date;
136 return "Can't change _date!" unless $old->_date == $new->_date;
137 return "Can't change charged!" unless $old->charged == $new->charged;
139 $new->SUPER::replace($old);
144 Checks all fields to make sure this is a valid invoice. If there is an error,
145 returns the error, otherwise returns false. Called by the insert and replace
154 $self->ut_numbern('invnum')
155 || $self->ut_number('custnum')
156 || $self->ut_numbern('_date')
157 || $self->ut_money('charged')
158 || $self->ut_numbern('printed')
159 || $self->ut_enum('closed', [ '', 'Y' ])
161 return $error if $error;
163 return "Unknown customer"
164 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
166 $self->_date(time) unless $self->_date;
168 $self->printed(0) if $self->printed eq '';
175 Returns a list consisting of the total previous balance for this customer,
176 followed by the previous outstanding invoices (as FS::cust_bill objects also).
183 my @cust_bill = sort { $a->_date <=> $b->_date }
184 grep { $_->owed != 0 && $_->_date < $self->_date }
185 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
187 foreach ( @cust_bill ) { $total += $_->owed; }
193 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
199 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
202 =item cust_bill_event
204 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
209 sub cust_bill_event {
211 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
217 Returns the customer (see L<FS::cust_main>) for this invoice.
223 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
228 Depreciated. See the cust_credited method.
230 #Returns a list consisting of the total previous credited (see
231 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
232 #outstanding credits (FS::cust_credit objects).
238 croak "FS::cust_bill->cust_credit depreciated; see ".
239 "FS::cust_bill->cust_credit_bill";
242 #my @cust_credit = sort { $a->_date <=> $b->_date }
243 # grep { $_->credited != 0 && $_->_date < $self->_date }
244 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
246 #foreach (@cust_credit) { $total += $_->credited; }
247 #$total, @cust_credit;
252 Depreciated. See the cust_bill_pay method.
254 #Returns all payments (see L<FS::cust_pay>) for this invoice.
260 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
262 #sort { $a->_date <=> $b->_date }
263 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
269 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
275 sort { $a->_date <=> $b->_date }
276 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
281 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
287 sort { $a->_date <=> $b->_date }
288 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
294 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
301 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
303 foreach (@taxlines) { $total += $_->setup; }
309 Returns the amount owed (still outstanding) on this invoice, which is charged
310 minus all payment applications (see L<FS::cust_bill_pay>) and credit
311 applications (see L<FS::cust_credit_bill>).
317 my $balance = $self->charged;
318 $balance -= $_->amount foreach ( $self->cust_bill_pay );
319 $balance -= $_->amount foreach ( $self->cust_credited );
320 $balance = sprintf( "%.2f", $balance);
321 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
326 =item generate_email PARAMHASH
328 PARAMHASH can contain the following:
332 =item from => sender address, required
334 =item tempate => alternate template name, optional
336 =item print_text => text attachment arrayref, optional
338 =item subject => email subject, optional
342 Returns an argument list to be passed to L<FS::Misc::send_email>.
353 my $me = '[FS::cust_bill::generate_email]';
356 'from' => $args{'from'},
357 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
360 if (ref($args{'to'} eq 'ARRAY')) {
361 $return{'to'} = $args{'to'};
363 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
364 $self->cust_main->invoicing_list
368 if ( $conf->exists('invoice_html') ) {
370 warn "$me creating HTML/text multipart message"
373 $return{'nobody'} = 1;
375 my $alternative = build MIME::Entity
376 'Type' => 'multipart/alternative',
377 'Encoding' => '7bit',
378 'Disposition' => 'inline'
382 if ( $conf->exists('invoice_email_pdf')
383 and scalar($conf->config('invoice_email_pdf_note')) ) {
385 warn "$me using 'invoice_email_pdf_note' in multipart message"
387 $data = [ map { $_ . "\n" }
388 $conf->config('invoice_email_pdf_note')
393 warn "$me not using 'invoice_email_pdf_note' in multipart message"
395 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
396 $data = $args{'print_text'};
398 $data = [ $self->print_text('', $args{'template'}) ];
403 $alternative->attach(
404 'Type' => 'text/plain',
405 #'Encoding' => 'quoted-printable',
406 'Encoding' => '7bit',
408 'Disposition' => 'inline',
411 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
412 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
414 my $image = build MIME::Entity
415 'Type' => 'image/png',
416 'Encoding' => 'base64',
417 'Path' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
418 'Filename' => 'logo.png',
419 'Content-ID' => "<$content_id>",
422 $alternative->attach(
423 'Type' => 'text/html',
424 'Encoding' => 'quoted-printable',
425 'Data' => [ '<html>',
428 ' '. encode_entities($return{'subject'}),
431 ' <body bgcolor="#e8e8e8">',
432 $self->print_html('', $args{'template'}, $content_id),
436 'Disposition' => 'inline',
437 #'Filename' => 'invoice.pdf',
440 if ( $conf->exists('invoice_email_pdf') ) {
445 # multipart/alternative
451 my $related = build MIME::Entity 'Type' => 'multipart/related',
452 'Encoding' => '7bit';
454 #false laziness w/Misc::send_email
455 $related->head->replace('Content-type',
457 '; boundary="'. $related->head->multipart_boundary. '"'.
458 '; type=multipart/alternative'
461 $related->add_part($alternative);
463 $related->add_part($image);
465 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
467 $return{'mimeparts'} = [ $related, $pdf ];
471 #no other attachment:
473 # multipart/alternative
478 $return{'content-type'} = 'multipart/related';
479 $return{'mimeparts'} = [ $alternative, $image ];
480 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
481 #$return{'disposition'} = 'inline';
487 if ( $conf->exists('invoice_email_pdf') ) {
488 warn "$me creating PDF attachment"
491 #mime parts arguments a la MIME::Entity->build().
492 $return{'mimeparts'} = [
493 { $self->mimebuild_pdf('', $args{'template'}) }
497 if ( $conf->exists('invoice_email_pdf')
498 and scalar($conf->config('invoice_email_pdf_note')) ) {
500 warn "$me using 'invoice_email_pdf_note'"
502 $return{'body'} = [ map { $_ . "\n" }
503 $conf->config('invoice_email_pdf_note')
508 warn "$me not using 'invoice_email_pdf_note'"
510 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
511 $return{'body'} = $args{'print_text'};
513 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
526 Returns a list suitable for passing to MIME::Entity->build(), representing
527 this invoice as PDF attachment.
534 'Type' => 'application/pdf',
535 'Encoding' => 'base64',
536 'Data' => [ $self->print_pdf(@_) ],
537 'Disposition' => 'attachment',
538 'Filename' => 'invoice.pdf',
542 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
544 Sends this invoice to the destinations configured for this customer: send
545 emails or print. See L<FS::cust_main_invoice>.
547 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
549 AGENTNUM, if specified, means that this invoice will only be sent for customers
550 of the specified agent.
552 INVOICE_FROM, if specified, overrides the default email invoice From: address.
558 my $template = scalar(@_) ? shift : '';
559 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
563 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
565 #my @print_text = $self->print_text('', $template);
566 my @invoicing_list = $self->cust_main->invoicing_list;
568 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) {
571 #better to notify this person than silence
572 @invoicing_list = ($invoice_from) unless @invoicing_list;
574 my $error = send_email(
575 $self->generate_email(
576 'from' => $invoice_from,
577 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
578 #'print_text' => [ @print_text ],
579 'template' => $template,
582 die "can't email invoice: $error\n" if $error;
583 #die "$error\n" if $error;
587 if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
589 if ($conf->config('invoice_latex')) {
590 $lpr_data = [ $self->print_ps('', $template) ];
592 $lpr_data = [ $self->print_text('', $template) ];
595 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
596 my $lpr = $conf->config('lpr');
598 or die "Can't open pipe to $lpr: $!\n";
599 print LPR @{$lpr_data};
601 or die $! ? "Error closing $lpr: $!\n"
602 : "Exit status $? from $lpr\n";
605 if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
606 die 'FAX invoice destination not supported with plain text invoices.'
607 unless $conf->exists('invoice_latex');
608 my $dialstring = $self->cust_main->getfield('fax');
610 my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
611 die $error if $error;
620 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
622 Like B<send>, but only sends the invoice if it is the newest open invoice for
632 grep { $_->owed > 0 }
633 qsearch('cust_bill', {
634 'custnum' => $self->custnum,
635 #'_date' => { op=>'>', value=>$self->_date },
636 'invnum' => { op=>'>', value=>$self->invnum },
643 =item send_csv OPTIONS
645 Sends invoice as a CSV data-file to a remote host with the specified protocol.
649 protocol - currently only "ftp"
655 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
656 and YYMMDDHHMMSS is a timestamp.
658 The fields of the CSV file is as follows:
660 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
664 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
666 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
667 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
668 fields are filled in.
670 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
671 first two fields (B<record_type> and B<invnum>) and the last five fields
672 (B<pkg> through B<edate>) are filled in.
674 =item invnum - invoice number
676 =item custnum - customer number
678 =item _date - invoice date
680 =item charged - total invoice amount
682 =item first - customer first name
684 =item last - customer first name
686 =item company - company name
688 =item address1 - address line 1
690 =item address2 - address line 1
700 =item pkg - line item description
702 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
704 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
706 =item sdate - start date for recurring fee
708 =item edate - end date for recurring fee
715 my($self, %opt) = @_;
717 #part one: create file
719 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
720 mkdir $spooldir, 0700 unless -d $spooldir;
722 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
724 open(CSV, ">$file") or die "can't open $file: $!";
726 eval "use Text::CSV_XS";
729 my $csv = Text::CSV_XS->new({'always_quote'=>1});
731 my $cust_main = $self->cust_main;
737 time2str("%x", $self->_date),
738 sprintf("%.2f", $self->charged),
739 ( map { $cust_main->getfield($_) }
740 qw( first last company address1 address2 city state zip country ) ),
742 ) or die "can't create csv";
743 print CSV $csv->string. "\n";
745 #new charges (false laziness w/print_text)
746 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
748 my($pkg, $setup, $recur, $sdate, $edate);
749 if ( $cust_bill_pkg->pkgnum ) {
751 ($pkg, $setup, $recur, $sdate, $edate) = (
752 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
753 ( $cust_bill_pkg->setup != 0
754 ? sprintf("%.2f", $cust_bill_pkg->setup )
756 ( $cust_bill_pkg->recur != 0
757 ? sprintf("%.2f", $cust_bill_pkg->recur )
759 time2str("%x", $cust_bill_pkg->sdate),
760 time2str("%x", $cust_bill_pkg->edate),
764 next unless $cust_bill_pkg->setup != 0;
765 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
766 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
768 ($pkg, $setup, $recur, $sdate, $edate) =
769 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
775 ( map { '' } (1..11) ),
776 ($pkg, $setup, $recur, $sdate, $edate)
777 ) or die "can't create csv";
778 print CSV $csv->string. "\n";
782 close CSV or die "can't close CSV: $!";
787 if ( $opt{protocol} eq 'ftp' ) {
788 eval "use Net::FTP;";
790 $net = Net::FTP->new($opt{server}) or die @$;
792 die "unknown protocol: $opt{protocol}";
795 $net->login( $opt{username}, $opt{password} )
796 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
798 $net->binary or die "can't set binary mode";
800 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
802 $net->put($file) or die "can't put $file: $!";
812 Pays this invoice with a compliemntary payment. If there is an error,
813 returns the error, otherwise returns false.
819 my $cust_pay = new FS::cust_pay ( {
820 'invnum' => $self->invnum,
821 'paid' => $self->owed,
824 'payinfo' => $self->cust_main->payinfo,
832 Attempts to pay this invoice with a credit card payment via a
833 Business::OnlinePayment realtime gateway. See
834 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
835 for supported processors.
841 $self->realtime_bop( 'CC', @_ );
846 Attempts to pay this invoice with an electronic check (ACH) payment via a
847 Business::OnlinePayment realtime gateway. See
848 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
849 for supported processors.
855 $self->realtime_bop( 'ECHECK', @_ );
860 Attempts to pay this invoice with phone bill (LEC) payment via a
861 Business::OnlinePayment realtime gateway. See
862 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
863 for supported processors.
869 $self->realtime_bop( 'LEC', @_ );
873 my( $self, $method ) = @_;
875 my $cust_main = $self->cust_main;
876 my $balance = $cust_main->balance;
877 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
878 $amount = sprintf("%.2f", $amount);
879 return "not run (balance $balance)" unless $amount > 0;
881 my $description = 'Internet Services';
882 if ( $conf->exists('business-onlinepayment-description') ) {
883 my $dtempl = $conf->config('business-onlinepayment-description');
885 my $agent_obj = $cust_main->agent
886 or die "can't retreive agent for $cust_main (agentnum ".
887 $cust_main->agentnum. ")";
888 my $agent = $agent_obj->agent;
889 my $pkgs = join(', ',
890 map { $_->cust_pkg->part_pkg->pkg }
891 grep { $_->pkgnum } $self->cust_bill_pkg
893 $description = eval qq("$dtempl");
896 $cust_main->realtime_bop($method, $amount,
897 'description' => $description,
898 'invnum' => $self->invnum,
905 Adds a payment for this invoice to the pending credit card batch (see
906 L<FS::cust_pay_batch>).
912 my $cust_main = $self->cust_main;
914 my $cust_pay_batch = new FS::cust_pay_batch ( {
915 'invnum' => $self->getfield('invnum'),
916 'custnum' => $cust_main->getfield('custnum'),
917 'last' => $cust_main->getfield('last'),
918 'first' => $cust_main->getfield('first'),
919 'address1' => $cust_main->getfield('address1'),
920 'address2' => $cust_main->getfield('address2'),
921 'city' => $cust_main->getfield('city'),
922 'state' => $cust_main->getfield('state'),
923 'zip' => $cust_main->getfield('zip'),
924 'country' => $cust_main->getfield('country'),
925 'cardnum' => $cust_main->payinfo,
926 'exp' => $cust_main->getfield('paydate'),
927 'payname' => $cust_main->getfield('payname'),
928 'amount' => $self->owed,
930 my $error = $cust_pay_batch->insert;
931 die $error if $error;
936 sub _agent_template {
938 $self->_agent_plandata('agent_templatename');
941 sub _agent_invoice_from {
943 $self->_agent_plandata('agent_invoice_from');
946 sub _agent_plandata {
947 my( $self, $option ) = @_;
949 my $part_bill_event = qsearchs( 'part_bill_event',
951 'payby' => $self->cust_main->payby,
952 'plan' => 'send_agent',
953 'plandata' => { 'op' => '~',
954 'value' => "(^|\n)agentnum ".
955 $self->cust_main->agentnum.
960 'ORDER BY seconds LIMIT 1'
963 return '' unless $part_bill_event;
965 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
968 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
969 " plandata for $option";
975 =item print_text [ TIME [ , TEMPLATE ] ]
977 Returns an text invoice, as a list of lines.
979 TIME an optional value used to control the printing of overdue messages. The
980 default is now. It isn't the date of the invoice; that's the `_date' field.
981 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
982 L<Time::Local> and L<Date::Parse> for conversion functions.
986 #still some false laziness w/print_text
989 my( $self, $today, $template ) = @_;
992 # my $invnum = $self->invnum;
993 my $cust_main = $self->cust_main;
994 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
995 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
997 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
998 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
999 #my $balance_due = $self->owed + $pr_total - $cr_total;
1000 my $balance_due = $self->owed + $pr_total;
1003 #my($description,$amount);
1007 foreach ( @pr_cust_bill ) {
1009 "Previous Balance, Invoice #". $_->invnum.
1010 " (". time2str("%x",$_->_date). ")",
1011 $money_char. sprintf("%10.2f",$_->owed)
1014 if (@pr_cust_bill) {
1015 push @buf,['','-----------'];
1016 push @buf,[ 'Total Previous Balance',
1017 $money_char. sprintf("%10.2f",$pr_total ) ];
1022 foreach my $cust_bill_pkg (
1023 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1024 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1027 if ( $cust_bill_pkg->pkgnum ) {
1029 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1030 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1031 my $pkg = $part_pkg->pkg;
1033 if ( $cust_bill_pkg->setup != 0 ) {
1034 my $description = $pkg;
1035 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1036 push @buf, [ $description,
1037 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1039 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1040 $cust_pkg->h_labels($self->_date);
1043 if ( $cust_bill_pkg->recur != 0 ) {
1045 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1046 time2str("%x", $cust_bill_pkg->edate) . ")",
1047 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1050 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1051 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1054 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1056 } else { #pkgnum tax or one-shot line item
1057 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1058 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1060 if ( $cust_bill_pkg->setup != 0 ) {
1061 push @buf, [ $itemdesc,
1062 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1064 if ( $cust_bill_pkg->recur != 0 ) {
1065 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1066 . time2str("%x", $cust_bill_pkg->edate). ")",
1067 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1073 push @buf,['','-----------'];
1074 push @buf,['Total New Charges',
1075 $money_char. sprintf("%10.2f",$self->charged) ];
1078 push @buf,['','-----------'];
1079 push @buf,['Total Charges',
1080 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1084 foreach ( $self->cust_credited ) {
1086 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1088 my $reason = substr($_->cust_credit->reason,0,32);
1089 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1090 $reason = " ($reason) " if $reason;
1092 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1094 $money_char. sprintf("%10.2f",$_->amount)
1097 #foreach ( @cr_cust_credit ) {
1099 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1100 # $money_char. sprintf("%10.2f",$_->credited)
1104 #get & print payments
1105 foreach ( $self->cust_bill_pay ) {
1107 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1110 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1111 $money_char. sprintf("%10.2f",$_->amount )
1116 my $balance_due_msg = $self->balance_due_msg;
1118 push @buf,['','-----------'];
1119 push @buf,[$balance_due_msg, $money_char.
1120 sprintf("%10.2f", $balance_due ) ];
1122 #create the template
1123 $template ||= $self->_agent_template;
1124 my $templatefile = 'invoice_template';
1125 $templatefile .= "_$template" if length($template);
1126 my @invoice_template = $conf->config($templatefile)
1127 or die "cannot load config file $templatefile";
1130 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1131 /invoice_lines\((\d*)\)/;
1132 $invoice_lines += $1 || scalar(@buf);
1135 die "no invoice_lines() functions in template?" unless $wasfunc;
1136 my $invoice_template = new Text::Template (
1138 SOURCE => [ map "$_\n", @invoice_template ],
1139 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1140 $invoice_template->compile()
1141 or die "can't compile template: $Text::Template::ERROR";
1143 #setup template variables
1144 package FS::cust_bill::_template; #!
1145 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1147 $invnum = $self->invnum;
1148 $date = $self->_date;
1150 $agent = $self->cust_main->agent->agent;
1152 if ( $FS::cust_bill::invoice_lines ) {
1154 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1156 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1161 #format address (variable for the template)
1163 @address = ( '', '', '', '', '', '' );
1164 package FS::cust_bill; #!
1165 $FS::cust_bill::_template::address[$l++] =
1166 $cust_main->payname.
1167 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1168 ? " (P.O. #". $cust_main->payinfo. ")"
1172 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1173 if $cust_main->company;
1174 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1175 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1176 if $cust_main->address2;
1177 $FS::cust_bill::_template::address[$l++] =
1178 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1180 my $countrydefault = $conf->config('countrydefault') || 'US';
1181 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1182 unless $cust_main->country eq $countrydefault;
1184 # #overdue? (variable for the template)
1185 # $FS::cust_bill::_template::overdue = (
1187 # && $today > $self->_date
1188 ## && $self->printed > 1
1189 # && $self->printed > 0
1192 #and subroutine for the template
1193 sub FS::cust_bill::_template::invoice_lines {
1194 my $lines = shift || scalar(@buf);
1196 scalar(@buf) ? shift @buf : [ '', '' ];
1202 $FS::cust_bill::_template::page = 1;
1206 push @collect, split("\n",
1207 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1209 $FS::cust_bill::_template::page++;
1212 map "$_\n", @collect;
1216 =item print_latex [ TIME [ , TEMPLATE ] ]
1218 Internal method - returns a filename of a filled-in LaTeX template for this
1219 invoice (Note: add ".tex" to get the actual filename).
1221 See print_ps and print_pdf for methods that return PostScript and PDF output.
1223 TIME an optional value used to control the printing of overdue messages. The
1224 default is now. It isn't the date of the invoice; that's the `_date' field.
1225 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1226 L<Time::Local> and L<Date::Parse> for conversion functions.
1230 #still some false laziness w/print_text
1233 my( $self, $today, $template ) = @_;
1235 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1238 my $cust_main = $self->cust_main;
1239 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1240 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1242 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1243 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1244 #my $balance_due = $self->owed + $pr_total - $cr_total;
1245 my $balance_due = $self->owed + $pr_total;
1247 #create the template
1248 $template ||= $self->_agent_template;
1249 my $templatefile = 'invoice_latex';
1250 my $suffix = length($template) ? "_$template" : '';
1251 $templatefile .= $suffix;
1252 my @invoice_template = map "$_\n", $conf->config($templatefile)
1253 or die "cannot load config file $templatefile";
1255 my($format, $text_template);
1256 if ( grep { /^%%Detail/ } @invoice_template ) {
1257 #change this to a die when the old code is removed
1258 warn "old-style invoice template $templatefile; ".
1259 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1262 $format = 'Text::Template';
1263 $text_template = new Text::Template(
1265 SOURCE => \@invoice_template,
1266 DELIMITERS => [ '[@--', '--@]' ],
1269 $text_template->compile()
1270 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1274 if ( $conf->exists('invoice_latexreturnaddress')
1275 && length($conf->exists('invoice_latexreturnaddress'))
1278 $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1280 $returnaddress = '~';
1283 my %invoice_data = (
1284 'invnum' => $self->invnum,
1285 'date' => time2str('%b %o, %Y', $self->_date),
1286 'today' => time2str('%b %o, %Y', $today),
1287 'agent' => _latex_escape($cust_main->agent->agent),
1288 'payname' => _latex_escape($cust_main->payname),
1289 'company' => _latex_escape($cust_main->company),
1290 'address1' => _latex_escape($cust_main->address1),
1291 'address2' => _latex_escape($cust_main->address2),
1292 'city' => _latex_escape($cust_main->city),
1293 'state' => _latex_escape($cust_main->state),
1294 'zip' => _latex_escape($cust_main->zip),
1295 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1296 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1297 'returnaddress' => $returnaddress,
1299 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1300 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1301 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1304 my $countrydefault = $conf->config('countrydefault') || 'US';
1305 if ( $cust_main->country eq $countrydefault ) {
1306 $invoice_data{'country'} = '';
1308 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1311 # #do variable substitutions in notes
1312 # $invoice_data{'notes'} =
1314 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1315 # $conf->config_orbase('invoice_latexnotes', $template)
1317 # warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1320 $invoice_data{'footer'} =~ s/\n+$//;
1321 $invoice_data{'smallfooter'} =~ s/\n+$//;
1322 $invoice_data{'notes'} =~ s/\n+$//;
1324 $invoice_data{'po_line'} =
1325 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1326 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1330 if ( $format eq 'old' ) {
1333 my @total_item = ();
1334 while ( @invoice_template ) {
1335 my $line = shift @invoice_template;
1337 if ( $line =~ /^%%Detail\s*$/ ) {
1339 while ( ( my $line_item_line = shift @invoice_template )
1340 !~ /^%%EndDetail\s*$/ ) {
1341 push @line_item, $line_item_line;
1343 foreach my $line_item ( $self->_items ) {
1344 #foreach my $line_item ( $self->_items_pkg ) {
1345 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1346 $invoice_data{'description'} =
1347 _latex_escape($line_item->{'description'});
1348 if ( exists $line_item->{'ext_description'} ) {
1349 $invoice_data{'description'} .=
1350 "\\tabularnewline\n~~".
1351 join( "\\tabularnewline\n~~",
1352 map _latex_escape($_), @{$line_item->{'ext_description'}}
1355 $invoice_data{'amount'} = $line_item->{'amount'};
1356 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1358 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1361 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1363 while ( ( my $total_item_line = shift @invoice_template )
1364 !~ /^%%EndTotalDetails\s*$/ ) {
1365 push @total_item, $total_item_line;
1368 my @total_fill = ();
1371 foreach my $tax ( $self->_items_tax ) {
1372 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1373 $taxtotal += $tax->{'amount'};
1374 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1376 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1381 $invoice_data{'total_item'} = 'Sub-total';
1382 $invoice_data{'total_amount'} =
1383 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1384 unshift @total_fill,
1385 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1389 $invoice_data{'total_item'} = '\textbf{Total}';
1390 $invoice_data{'total_amount'} =
1391 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1393 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1396 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1399 foreach my $credit ( $self->_items_credits ) {
1400 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1402 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1404 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1409 foreach my $payment ( $self->_items_payments ) {
1410 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1412 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1414 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1418 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1419 $invoice_data{'total_amount'} =
1420 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1422 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1425 push @filled_in, @total_fill;
1428 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1429 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1430 push @filled_in, $line;
1441 } elsif ( $format eq 'Text::Template' ) {
1443 my @detail_items = ();
1444 my @total_items = ();
1446 $invoice_data{'detail_items'} = \@detail_items;
1447 $invoice_data{'total_items'} = \@total_items;
1449 foreach my $line_item ( $self->_items ) {
1451 ext_description => [],
1453 $detail->{'ref'} = $line_item->{'pkgnum'};
1454 $detail->{'quantity'} = 1;
1455 $detail->{'description'} = _latex_escape($line_item->{'description'});
1456 if ( exists $line_item->{'ext_description'} ) {
1457 @{$detail->{'ext_description'}} = map {
1459 } @{$line_item->{'ext_description'}};
1461 $detail->{'amount'} = $line_item->{'amount'};
1462 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1464 push @detail_items, $detail;
1469 foreach my $tax ( $self->_items_tax ) {
1471 $total->{'total_item'} = _latex_escape($tax->{'description'});
1472 $taxtotal += $tax->{'amount'};
1473 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1474 push @total_items, $total;
1479 $total->{'total_item'} = 'Sub-total';
1480 $total->{'total_amount'} =
1481 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1482 unshift @total_items, $total;
1487 $total->{'total_item'} = '\textbf{Total}';
1488 $total->{'total_amount'} =
1489 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1490 push @total_items, $total;
1493 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1496 foreach my $credit ( $self->_items_credits ) {
1498 $total->{'total_item'} = _latex_escape($credit->{'description'});
1500 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1501 push @total_items, $total;
1505 foreach my $payment ( $self->_items_payments ) {
1507 $total->{'total_item'} = _latex_escape($payment->{'description'});
1509 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1510 push @total_items, $total;
1515 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1516 $total->{'total_amount'} =
1517 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1518 push @total_items, $total;
1522 die "guru meditation #54";
1525 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1526 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1530 ) or die "can't open temp file: $!\n";
1531 if ( $format eq 'old' ) {
1532 print $fh join('', @filled_in );
1533 } elsif ( $format eq 'Text::Template' ) {
1534 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1536 die "guru meditation #32";
1540 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1545 =item print_ps [ TIME [ , TEMPLATE ] ]
1547 Returns an postscript invoice, as a scalar.
1549 TIME an optional value used to control the printing of overdue messages. The
1550 default is now. It isn't the date of the invoice; that's the `_date' field.
1551 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1552 L<Time::Local> and L<Date::Parse> for conversion functions.
1559 my $file = $self->print_latex(@_);
1561 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1564 my $sfile = shell_quote $file;
1566 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1567 or die "pslatex $file.tex failed; see $file.log for details?\n";
1568 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1569 or die "pslatex $file.tex failed; see $file.log for details?\n";
1571 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1572 or die "dvips failed";
1574 open(POSTSCRIPT, "<$file.ps")
1575 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1577 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1580 while (<POSTSCRIPT>) {
1590 =item print_pdf [ TIME [ , TEMPLATE ] ]
1592 Returns an PDF invoice, as a scalar.
1594 TIME an optional value used to control the printing of overdue messages. The
1595 default is now. It isn't the date of the invoice; that's the `_date' field.
1596 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1597 L<Time::Local> and L<Date::Parse> for conversion functions.
1604 my $file = $self->print_latex(@_);
1606 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1609 #system('pdflatex', "$file.tex");
1610 #system('pdflatex', "$file.tex");
1611 #! LaTeX Error: Unknown graphics extension: .eps.
1613 my $sfile = shell_quote $file;
1615 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1616 or die "pslatex $file.tex failed; see $file.log for details?\n";
1617 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1618 or die "pslatex $file.tex failed; see $file.log for details?\n";
1620 #system('dvipdf', "$file.dvi", "$file.pdf" );
1622 "dvips -q -t letter -f $sfile.dvi ".
1623 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1626 or die "dvips | gs failed: $!";
1628 open(PDF, "<$file.pdf")
1629 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1631 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1644 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1646 Returns an HTML invoice, as a scalar.
1648 TIME an optional value used to control the printing of overdue messages. The
1649 default is now. It isn't the date of the invoice; that's the `_date' field.
1650 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1651 L<Time::Local> and L<Date::Parse> for conversion functions.
1653 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1654 when emailing the invoice as part of a multipart/related MIME email.
1659 my( $self, $today, $template, $cid ) = @_;
1662 my $cust_main = $self->cust_main;
1663 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1664 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1666 $template ||= $self->_agent_template;
1667 my $templatefile = 'invoice_html';
1668 my $suffix = length($template) ? "_$template" : '';
1669 $templatefile .= $suffix;
1670 my @html_template = map "$_\n", $conf->config($templatefile)
1671 or die "cannot load config file $templatefile";
1673 my $html_template = new Text::Template(
1675 SOURCE => \@html_template,
1676 DELIMITERS => [ '<%=', '%>' ],
1679 $html_template->compile()
1680 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1682 my %invoice_data = (
1683 'invnum' => $self->invnum,
1684 'date' => time2str('%b %o, %Y', $self->_date),
1685 'today' => time2str('%b %o, %Y', $today),
1686 'agent' => encode_entities($cust_main->agent->agent),
1687 'payname' => encode_entities($cust_main->payname),
1688 'company' => encode_entities($cust_main->company),
1689 'address1' => encode_entities($cust_main->address1),
1690 'address2' => encode_entities($cust_main->address2),
1691 'city' => encode_entities($cust_main->city),
1692 'state' => encode_entities($cust_main->state),
1693 'zip' => encode_entities($cust_main->zip),
1694 'terms' => $conf->config('invoice_default_terms')
1695 || 'Payable upon receipt',
1697 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1700 $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
1701 ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1702 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1703 $conf->config('invoice_latexreturnaddress')
1706 my $countrydefault = $conf->config('countrydefault') || 'US';
1707 if ( $cust_main->country eq $countrydefault ) {
1708 $invoice_data{'country'} = '';
1710 $invoice_data{'country'} =
1711 encode_entities(code2country($cust_main->country));
1714 $invoice_data{'notes'} =
1715 length($conf->config_orbase('invoice_htmlnotes', $template))
1716 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1718 s/%%(.*)$/<!-- $1 -->/;
1719 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1720 s/\\begin\{enumerate\}/<ol>/;
1722 s/\\end\{enumerate\}/<\/ol>/;
1723 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1726 $conf->config_orbase('invoice_latexnotes', $template)
1729 # #do variable substitutions in notes
1730 # $invoice_data{'notes'} =
1732 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1733 # $conf->config_orbase('invoice_latexnotes', $suffix)
1736 $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1737 ? join("\n", $conf->config('invoice_htmlfooter') )
1738 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1739 $conf->config('invoice_latexfooter')
1742 $invoice_data{'po_line'} =
1743 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1744 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1747 my $money_char = $conf->config('money_char') || '$';
1749 foreach my $line_item ( $self->_items ) {
1751 ext_description => [],
1753 $detail->{'ref'} = $line_item->{'pkgnum'};
1754 $detail->{'description'} = encode_entities($line_item->{'description'});
1755 if ( exists $line_item->{'ext_description'} ) {
1756 @{$detail->{'ext_description'}} = map {
1757 encode_entities($_);
1758 } @{$line_item->{'ext_description'}};
1760 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1761 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1763 push @{$invoice_data{'detail_items'}}, $detail;
1768 foreach my $tax ( $self->_items_tax ) {
1770 $total->{'total_item'} = encode_entities($tax->{'description'});
1771 $taxtotal += $tax->{'amount'};
1772 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1773 push @{$invoice_data{'total_items'}}, $total;
1778 $total->{'total_item'} = 'Sub-total';
1779 $total->{'total_amount'} =
1780 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1781 unshift @{$invoice_data{'total_items'}}, $total;
1784 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1787 $total->{'total_item'} = '<b>Total</b>';
1788 $total->{'total_amount'} =
1789 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1790 push @{$invoice_data{'total_items'}}, $total;
1793 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1796 foreach my $credit ( $self->_items_credits ) {
1798 $total->{'total_item'} = encode_entities($credit->{'description'});
1800 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1801 push @{$invoice_data{'total_items'}}, $total;
1805 foreach my $payment ( $self->_items_payments ) {
1807 $total->{'total_item'} = encode_entities($payment->{'description'});
1809 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1810 push @{$invoice_data{'total_items'}}, $total;
1815 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1816 $total->{'total_amount'} =
1817 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1818 push @{$invoice_data{'total_items'}}, $total;
1821 $html_template->fill_in( HASH => \%invoice_data);
1824 # quick subroutine for print_latex
1826 # There are ten characters that LaTeX treats as special characters, which
1827 # means that they do not simply typeset themselves:
1828 # # $ % & ~ _ ^ \ { }
1830 # TeX ignores blanks following an escaped character; if you want a blank (as
1831 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1835 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1836 $value =~ s/([<>])/\$$1\$/g;
1840 #utility methods for print_*
1842 sub balance_due_msg {
1844 my $msg = 'Balance Due';
1845 return $msg unless $conf->exists('invoice_default_terms');
1846 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1847 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1848 } elsif ( $conf->config('invoice_default_terms') ) {
1849 $msg .= ' - '. $conf->config('invoice_default_terms');
1856 my @display = scalar(@_)
1858 : qw( _items_previous _items_pkg );
1859 #: qw( _items_pkg );
1860 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1862 foreach my $display ( @display ) {
1863 push @b, $self->$display(@_);
1868 sub _items_previous {
1870 my $cust_main = $self->cust_main;
1871 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1873 foreach ( @pr_cust_bill ) {
1875 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1876 ' ('. time2str('%x',$_->_date). ')',
1877 #'pkgpart' => 'N/A',
1879 'amount' => sprintf("%.2f", $_->owed),
1885 # 'description' => 'Previous Balance',
1886 # #'pkgpart' => 'N/A',
1887 # 'pkgnum' => 'N/A',
1888 # 'amount' => sprintf("%10.2f", $pr_total ),
1889 # 'ext_description' => [ map {
1890 # "Invoice ". $_->invnum.
1891 # " (". time2str("%x",$_->_date). ") ".
1892 # sprintf("%10.2f", $_->owed)
1893 # } @pr_cust_bill ],
1900 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1901 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1906 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1907 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1910 sub _items_cust_bill_pkg {
1912 my $cust_bill_pkg = shift;
1915 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1917 if ( $cust_bill_pkg->pkgnum ) {
1919 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1920 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1921 my $pkg = $part_pkg->pkg;
1923 if ( $cust_bill_pkg->setup != 0 ) {
1924 my $description = $pkg;
1925 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1926 my @d = $cust_pkg->h_labels_short($self->_date);
1927 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1929 description => $description,
1930 #pkgpart => $part_pkg->pkgpart,
1931 pkgnum => $cust_pkg->pkgnum,
1932 amount => sprintf("%.2f", $cust_bill_pkg->setup),
1933 ext_description => \@d,
1937 if ( $cust_bill_pkg->recur != 0 ) {
1939 description => "$pkg (" .
1940 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1941 time2str('%x', $cust_bill_pkg->edate). ')',
1942 #pkgpart => $part_pkg->pkgpart,
1943 pkgnum => $cust_pkg->pkgnum,
1944 amount => sprintf("%.2f", $cust_bill_pkg->recur),
1945 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1946 $cust_bill_pkg->sdate),
1947 $cust_bill_pkg->details,
1952 } else { #pkgnum tax or one-shot line item (??)
1954 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1955 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1957 if ( $cust_bill_pkg->setup != 0 ) {
1959 'description' => $itemdesc,
1960 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
1963 if ( $cust_bill_pkg->recur != 0 ) {
1965 'description' => "$itemdesc (".
1966 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1967 time2str("%x", $cust_bill_pkg->edate). ')',
1968 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
1980 sub _items_credits {
1985 foreach ( $self->cust_credited ) {
1987 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1989 my $reason = $_->cust_credit->reason;
1990 #my $reason = substr($_->cust_credit->reason,0,32);
1991 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1992 $reason = " ($reason) " if $reason;
1994 #'description' => 'Credit ref\#'. $_->crednum.
1995 # " (". time2str("%x",$_->cust_credit->_date) .")".
1997 'description' => 'Credit applied '.
1998 time2str("%x",$_->cust_credit->_date). $reason,
1999 'amount' => sprintf("%.2f",$_->amount),
2002 #foreach ( @cr_cust_credit ) {
2004 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2005 # $money_char. sprintf("%10.2f",$_->credited)
2013 sub _items_payments {
2017 #get & print payments
2018 foreach ( $self->cust_bill_pay ) {
2020 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2023 'description' => "Payment received ".
2024 time2str("%x",$_->cust_pay->_date ),
2025 'amount' => sprintf("%.2f", $_->amount )
2039 print_text formatting (and some logic :/) is in source, but needs to be
2040 slurped in from a file. Also number of lines ($=).
2044 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2045 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base