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>.
352 if ($conf->exists('invoice_email_pdf')) {
353 #warn "[FS::cust_bill::send] creating PDF attachment";
354 #mime parts arguments a la MIME::Entity->build().
357 'Type' => 'application/pdf',
358 'Encoding' => 'base64',
359 'Data' => [ $self->print_pdf('', $args{'template'}) ],
360 'Disposition' => 'attachment',
361 'Filename' => 'invoice.pdf',
367 if ($conf->exists('invoice_email_pdf')
368 and scalar($conf->config('invoice_email_pdf_note'))) {
370 #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'";
371 $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ];
373 #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'";
374 if (ref($args{'print_text'}) eq 'ARRAY') {
375 $email_text = $args{'print_text'};
377 $email_text = [ $self->print_text('', $args{'template'}) ];
382 if (ref($args{'to'} eq 'ARRAY')) {
383 @invoicing_list = @{$args{'to'}};
385 @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
389 'from' => $args{'from'},
390 'to' => [ @invoicing_list ],
391 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
392 'body' => $email_text,
393 'mimeparts' => $mimeparts,
399 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
401 Sends this invoice to the destinations configured for this customer: send
402 emails or print. See L<FS::cust_main_invoice>.
404 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
406 AGENTNUM, if specified, means that this invoice will only be sent for customers
407 of the specified agent.
409 INVOICE_FROM, if specified, overrides the default email invoice From: address.
415 my $template = scalar(@_) ? shift : '';
416 return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
420 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
422 my @print_text = $self->print_text('', $template);
423 my @invoicing_list = $self->cust_main->invoicing_list;
425 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) {
428 #better to notify this person than silence
429 @invoicing_list = ($invoice_from) unless @invoicing_list;
431 my $error = send_email(
432 $self->generate_email(
433 'from' => $invoice_from,
434 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
435 'print_text' => [ @print_text ],
438 die "can't email invoice: $error\n" if $error;
439 #die "$error\n" if $error;
443 if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
445 if ($conf->config('invoice_latex')) {
446 $lpr_data = [ $self->print_ps('', $template) ];
448 $lpr_data = \@print_text;
451 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
452 my $lpr = $conf->config('lpr');
454 or die "Can't open pipe to $lpr: $!\n";
455 print LPR @{$lpr_data};
457 or die $! ? "Error closing $lpr: $!\n"
458 : "Exit status $? from $lpr\n";
461 if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
462 die 'FAX invoice destination not supported with plain text invoices.'
463 unless $conf->exists('invoice_latex');
464 my $dialstring = $self->cust_main->getfield('fax');
466 my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
467 die $error if $error;
476 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
478 Like B<send>, but only sends the invoice if it is the newest open invoice for
488 grep { $_->owed > 0 }
489 qsearch('cust_bill', {
490 'custnum' => $self->custnum,
491 #'_date' => { op=>'>', value=>$self->_date },
492 'invnum' => { op=>'>', value=>$self->invnum },
499 =item send_csv OPTIONS
501 Sends invoice as a CSV data-file to a remote host with the specified protocol.
505 protocol - currently only "ftp"
511 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
512 and YYMMDDHHMMSS is a timestamp.
514 The fields of the CSV file is as follows:
516 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
520 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
522 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
523 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
524 fields are filled in.
526 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
527 first two fields (B<record_type> and B<invnum>) and the last five fields
528 (B<pkg> through B<edate>) are filled in.
530 =item invnum - invoice number
532 =item custnum - customer number
534 =item _date - invoice date
536 =item charged - total invoice amount
538 =item first - customer first name
540 =item last - customer first name
542 =item company - company name
544 =item address1 - address line 1
546 =item address2 - address line 1
556 =item pkg - line item description
558 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
560 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
562 =item sdate - start date for recurring fee
564 =item edate - end date for recurring fee
571 my($self, %opt) = @_;
573 #part one: create file
575 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
576 mkdir $spooldir, 0700 unless -d $spooldir;
578 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
580 open(CSV, ">$file") or die "can't open $file: $!";
582 eval "use Text::CSV_XS";
585 my $csv = Text::CSV_XS->new({'always_quote'=>1});
587 my $cust_main = $self->cust_main;
593 time2str("%x", $self->_date),
594 sprintf("%.2f", $self->charged),
595 ( map { $cust_main->getfield($_) }
596 qw( first last company address1 address2 city state zip country ) ),
598 ) or die "can't create csv";
599 print CSV $csv->string. "\n";
601 #new charges (false laziness w/print_text)
602 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
604 my($pkg, $setup, $recur, $sdate, $edate);
605 if ( $cust_bill_pkg->pkgnum ) {
607 ($pkg, $setup, $recur, $sdate, $edate) = (
608 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
609 ( $cust_bill_pkg->setup != 0
610 ? sprintf("%.2f", $cust_bill_pkg->setup )
612 ( $cust_bill_pkg->recur != 0
613 ? sprintf("%.2f", $cust_bill_pkg->recur )
615 time2str("%x", $cust_bill_pkg->sdate),
616 time2str("%x", $cust_bill_pkg->edate),
620 next unless $cust_bill_pkg->setup != 0;
621 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
622 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
624 ($pkg, $setup, $recur, $sdate, $edate) =
625 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
631 ( map { '' } (1..11) ),
632 ($pkg, $setup, $recur, $sdate, $edate)
633 ) or die "can't create csv";
634 print CSV $csv->string. "\n";
638 close CSV or die "can't close CSV: $!";
643 if ( $opt{protocol} eq 'ftp' ) {
644 eval "use Net::FTP;";
646 $net = Net::FTP->new($opt{server}) or die @$;
648 die "unknown protocol: $opt{protocol}";
651 $net->login( $opt{username}, $opt{password} )
652 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
654 $net->binary or die "can't set binary mode";
656 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
658 $net->put($file) or die "can't put $file: $!";
668 Pays this invoice with a compliemntary payment. If there is an error,
669 returns the error, otherwise returns false.
675 my $cust_pay = new FS::cust_pay ( {
676 'invnum' => $self->invnum,
677 'paid' => $self->owed,
680 'payinfo' => $self->cust_main->payinfo,
688 Attempts to pay this invoice with a credit card payment via a
689 Business::OnlinePayment realtime gateway. See
690 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
691 for supported processors.
697 $self->realtime_bop( 'CC', @_ );
702 Attempts to pay this invoice with an electronic check (ACH) payment via a
703 Business::OnlinePayment realtime gateway. See
704 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
705 for supported processors.
711 $self->realtime_bop( 'ECHECK', @_ );
716 Attempts to pay this invoice with phone bill (LEC) payment via a
717 Business::OnlinePayment realtime gateway. See
718 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
719 for supported processors.
725 $self->realtime_bop( 'LEC', @_ );
729 my( $self, $method ) = @_;
731 my $cust_main = $self->cust_main;
732 my $balance = $cust_main->balance;
733 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
734 $amount = sprintf("%.2f", $amount);
735 return "not run (balance $balance)" unless $amount > 0;
737 my $description = 'Internet Services';
738 if ( $conf->exists('business-onlinepayment-description') ) {
739 my $dtempl = $conf->config('business-onlinepayment-description');
741 my $agent_obj = $cust_main->agent
742 or die "can't retreive agent for $cust_main (agentnum ".
743 $cust_main->agentnum. ")";
744 my $agent = $agent_obj->agent;
745 my $pkgs = join(', ',
746 map { $_->cust_pkg->part_pkg->pkg }
747 grep { $_->pkgnum } $self->cust_bill_pkg
749 $description = eval qq("$dtempl");
752 $cust_main->realtime_bop($method, $amount,
753 'description' => $description,
754 'invnum' => $self->invnum,
761 Adds a payment for this invoice to the pending credit card batch (see
762 L<FS::cust_pay_batch>).
768 my $cust_main = $self->cust_main;
770 my $cust_pay_batch = new FS::cust_pay_batch ( {
771 'invnum' => $self->getfield('invnum'),
772 'custnum' => $cust_main->getfield('custnum'),
773 'last' => $cust_main->getfield('last'),
774 'first' => $cust_main->getfield('first'),
775 'address1' => $cust_main->getfield('address1'),
776 'address2' => $cust_main->getfield('address2'),
777 'city' => $cust_main->getfield('city'),
778 'state' => $cust_main->getfield('state'),
779 'zip' => $cust_main->getfield('zip'),
780 'country' => $cust_main->getfield('country'),
781 'cardnum' => $cust_main->payinfo,
782 'exp' => $cust_main->getfield('paydate'),
783 'payname' => $cust_main->getfield('payname'),
784 'amount' => $self->owed,
786 my $error = $cust_pay_batch->insert;
787 die $error if $error;
792 sub _agent_template {
794 $self->_agent_plandata('agent_templatename');
797 sub _agent_invoice_from {
799 $self->_agent_plandata('agent_invoice_from');
802 sub _agent_plandata {
803 my( $self, $option ) = @_;
805 my $part_bill_event = qsearchs( 'part_bill_event',
807 'payby' => $self->cust_main->payby,
808 'plan' => 'send_agent',
809 'plandata' => { 'op' => '~',
810 'value' => "(^|\n)agentnum ".
811 $self->cust_main->agentnum.
816 'ORDER BY seconds LIMIT 1'
819 return '' unless $part_bill_event;
821 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
824 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
825 " plandata for $option";
831 =item print_text [ TIME [ , TEMPLATE ] ]
833 Returns an text invoice, as a list of lines.
835 TIME an optional value used to control the printing of overdue messages. The
836 default is now. It isn't the date of the invoice; that's the `_date' field.
837 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
838 L<Time::Local> and L<Date::Parse> for conversion functions.
842 #still some false laziness w/print_text
845 my( $self, $today, $template ) = @_;
848 # my $invnum = $self->invnum;
849 my $cust_main = $self->cust_main;
850 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
851 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
853 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
854 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
855 #my $balance_due = $self->owed + $pr_total - $cr_total;
856 my $balance_due = $self->owed + $pr_total;
859 #my($description,$amount);
863 foreach ( @pr_cust_bill ) {
865 "Previous Balance, Invoice #". $_->invnum.
866 " (". time2str("%x",$_->_date). ")",
867 $money_char. sprintf("%10.2f",$_->owed)
871 push @buf,['','-----------'];
872 push @buf,[ 'Total Previous Balance',
873 $money_char. sprintf("%10.2f",$pr_total ) ];
878 foreach my $cust_bill_pkg (
879 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
880 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
883 if ( $cust_bill_pkg->pkgnum ) {
885 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
886 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
887 my $pkg = $part_pkg->pkg;
889 if ( $cust_bill_pkg->setup != 0 ) {
890 my $description = $pkg;
891 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
892 push @buf, [ $description,
893 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
895 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
896 $cust_pkg->h_labels($self->_date);
899 if ( $cust_bill_pkg->recur != 0 ) {
901 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
902 time2str("%x", $cust_bill_pkg->edate) . ")",
903 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
906 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
907 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
910 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
912 } else { #pkgnum tax or one-shot line item
913 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
914 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
916 if ( $cust_bill_pkg->setup != 0 ) {
917 push @buf, [ $itemdesc,
918 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
920 if ( $cust_bill_pkg->recur != 0 ) {
921 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
922 . time2str("%x", $cust_bill_pkg->edate). ")",
923 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
929 push @buf,['','-----------'];
930 push @buf,['Total New Charges',
931 $money_char. sprintf("%10.2f",$self->charged) ];
934 push @buf,['','-----------'];
935 push @buf,['Total Charges',
936 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
940 foreach ( $self->cust_credited ) {
942 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
944 my $reason = substr($_->cust_credit->reason,0,32);
945 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
946 $reason = " ($reason) " if $reason;
948 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
950 $money_char. sprintf("%10.2f",$_->amount)
953 #foreach ( @cr_cust_credit ) {
955 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
956 # $money_char. sprintf("%10.2f",$_->credited)
960 #get & print payments
961 foreach ( $self->cust_bill_pay ) {
963 #something more elaborate if $_->amount ne ->cust_pay->paid ?
966 "Payment received ". time2str("%x",$_->cust_pay->_date ),
967 $money_char. sprintf("%10.2f",$_->amount )
972 my $balance_due_msg = $self->balance_due_msg;
974 push @buf,['','-----------'];
975 push @buf,[$balance_due_msg, $money_char.
976 sprintf("%10.2f", $balance_due ) ];
979 $template ||= $self->_agent_template;
980 my $templatefile = 'invoice_template';
981 $templatefile .= "_$template" if length($template);
982 my @invoice_template = $conf->config($templatefile)
983 or die "cannot load config file $templatefile";
986 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
987 /invoice_lines\((\d*)\)/;
988 $invoice_lines += $1 || scalar(@buf);
991 die "no invoice_lines() functions in template?" unless $wasfunc;
992 my $invoice_template = new Text::Template (
994 SOURCE => [ map "$_\n", @invoice_template ],
995 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
996 $invoice_template->compile()
997 or die "can't compile template: $Text::Template::ERROR";
999 #setup template variables
1000 package FS::cust_bill::_template; #!
1001 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1003 $invnum = $self->invnum;
1004 $date = $self->_date;
1006 $agent = $self->cust_main->agent->agent;
1008 if ( $FS::cust_bill::invoice_lines ) {
1010 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1012 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1017 #format address (variable for the template)
1019 @address = ( '', '', '', '', '', '' );
1020 package FS::cust_bill; #!
1021 $FS::cust_bill::_template::address[$l++] =
1022 $cust_main->payname.
1023 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1024 ? " (P.O. #". $cust_main->payinfo. ")"
1028 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1029 if $cust_main->company;
1030 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1031 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1032 if $cust_main->address2;
1033 $FS::cust_bill::_template::address[$l++] =
1034 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1036 my $countrydefault = $conf->config('countrydefault') || 'US';
1037 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1038 unless $cust_main->country eq $countrydefault;
1040 # #overdue? (variable for the template)
1041 # $FS::cust_bill::_template::overdue = (
1043 # && $today > $self->_date
1044 ## && $self->printed > 1
1045 # && $self->printed > 0
1048 #and subroutine for the template
1049 sub FS::cust_bill::_template::invoice_lines {
1050 my $lines = shift || scalar(@buf);
1052 scalar(@buf) ? shift @buf : [ '', '' ];
1058 $FS::cust_bill::_template::page = 1;
1062 push @collect, split("\n",
1063 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1065 $FS::cust_bill::_template::page++;
1068 map "$_\n", @collect;
1072 =item print_latex [ TIME [ , TEMPLATE ] ]
1074 Internal method - returns a filename of a filled-in LaTeX template for this
1075 invoice (Note: add ".tex" to get the actual filename).
1077 See print_ps and print_pdf for methods that return PostScript and PDF output.
1079 TIME an optional value used to control the printing of overdue messages. The
1080 default is now. It isn't the date of the invoice; that's the `_date' field.
1081 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1082 L<Time::Local> and L<Date::Parse> for conversion functions.
1086 #still some false laziness w/print_text
1089 my( $self, $today, $template ) = @_;
1091 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1094 my $cust_main = $self->cust_main;
1095 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1096 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1098 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1099 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1100 #my $balance_due = $self->owed + $pr_total - $cr_total;
1101 my $balance_due = $self->owed + $pr_total;
1103 #create the template
1104 $template ||= $self->_agent_template;
1105 my $templatefile = 'invoice_latex';
1106 my $suffix = length($template) ? "_$template" : '';
1107 $templatefile .= $suffix;
1108 my @invoice_template = map "$_\n", $conf->config($templatefile)
1109 or die "cannot load config file $templatefile";
1111 my($format, $text_template);
1112 if ( grep { /^%%Detail/ } @invoice_template ) {
1113 #change this to a die when the old code is removed
1114 warn "old-style invoice template $templatefile; ".
1115 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1118 $format = 'Text::Template';
1119 $text_template = new Text::Template(
1121 SOURCE => \@invoice_template,
1122 DELIMITERS => [ '[@--', '--@]' ],
1125 $text_template->compile()
1126 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1130 if ( $conf->exists('invoice_latexreturnaddress')
1131 && length($conf->exists('invoice_latexreturnaddress'))
1134 $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1136 $returnaddress = '~';
1139 my %invoice_data = (
1140 'invnum' => $self->invnum,
1141 'date' => time2str('%b %o, %Y', $self->_date),
1142 'today' => time2str('%b %o, %Y', $today),
1143 'agent' => _latex_escape($cust_main->agent->agent),
1144 'payname' => _latex_escape($cust_main->payname),
1145 'company' => _latex_escape($cust_main->company),
1146 'address1' => _latex_escape($cust_main->address1),
1147 'address2' => _latex_escape($cust_main->address2),
1148 'city' => _latex_escape($cust_main->city),
1149 'state' => _latex_escape($cust_main->state),
1150 'zip' => _latex_escape($cust_main->zip),
1151 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1152 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1153 'returnaddress' => $returnaddress,
1155 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1156 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1157 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1160 my $countrydefault = $conf->config('countrydefault') || 'US';
1161 if ( $cust_main->country eq $countrydefault ) {
1162 $invoice_data{'country'} = '';
1164 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1167 #do variable substitutions in notes
1168 $invoice_data{'notes'} =
1170 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1171 $conf->config_orbase('invoice_latexnotes', $template)
1173 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1176 $invoice_data{'footer'} =~ s/\n+$//;
1177 $invoice_data{'smallfooter'} =~ s/\n+$//;
1178 $invoice_data{'notes'} =~ s/\n+$//;
1180 $invoice_data{'po_line'} =
1181 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1182 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1186 if ( $format eq 'old' ) {
1189 my @total_item = ();
1190 while ( @invoice_template ) {
1191 my $line = shift @invoice_template;
1193 if ( $line =~ /^%%Detail\s*$/ ) {
1195 while ( ( my $line_item_line = shift @invoice_template )
1196 !~ /^%%EndDetail\s*$/ ) {
1197 push @line_item, $line_item_line;
1199 foreach my $line_item ( $self->_items ) {
1200 #foreach my $line_item ( $self->_items_pkg ) {
1201 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1202 $invoice_data{'description'} =
1203 _latex_escape($line_item->{'description'});
1204 if ( exists $line_item->{'ext_description'} ) {
1205 $invoice_data{'description'} .=
1206 "\\tabularnewline\n~~".
1207 join( "\\tabularnewline\n~~",
1208 map _latex_escape($_), @{$line_item->{'ext_description'}}
1211 $invoice_data{'amount'} = $line_item->{'amount'};
1212 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1214 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1217 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1219 while ( ( my $total_item_line = shift @invoice_template )
1220 !~ /^%%EndTotalDetails\s*$/ ) {
1221 push @total_item, $total_item_line;
1224 my @total_fill = ();
1227 foreach my $tax ( $self->_items_tax ) {
1228 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1229 $taxtotal += $tax->{'amount'};
1230 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1232 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1237 $invoice_data{'total_item'} = 'Sub-total';
1238 $invoice_data{'total_amount'} =
1239 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1240 unshift @total_fill,
1241 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1245 $invoice_data{'total_item'} = '\textbf{Total}';
1246 $invoice_data{'total_amount'} =
1247 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1249 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1252 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1255 foreach my $credit ( $self->_items_credits ) {
1256 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1258 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1260 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1265 foreach my $payment ( $self->_items_payments ) {
1266 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1268 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1270 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1274 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1275 $invoice_data{'total_amount'} =
1276 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1278 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1281 push @filled_in, @total_fill;
1284 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1285 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1286 push @filled_in, $line;
1297 } elsif ( $format eq 'Text::Template' ) {
1299 my @detail_items = ();
1300 my @total_items = ();
1302 $invoice_data{'detail_items'} = \@detail_items;
1303 $invoice_data{'total_items'} = \@total_items;
1305 foreach my $line_item ( $self->_items ) {
1307 ext_description => [],
1309 $detail->{'ref'} = $line_item->{'pkgnum'};
1310 $detail->{'quantity'} = 1;
1311 $detail->{'description'} = _latex_escape($line_item->{'description'});
1312 if ( exists $line_item->{'ext_description'} ) {
1313 @{$detail->{'ext_description'}} = map {
1315 } @{$line_item->{'ext_description'}};
1317 $detail->{'amount'} = $line_item->{'amount'};
1318 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1320 push @detail_items, $detail;
1325 foreach my $tax ( $self->_items_tax ) {
1327 $total->{'total_item'} = _latex_escape($tax->{'description'});
1328 $taxtotal += $tax->{'amount'};
1329 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1330 push @total_items, $total;
1335 $total->{'total_item'} = 'Sub-total';
1336 $total->{'total_amount'} =
1337 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1338 unshift @total_items, $total;
1343 $total->{'total_item'} = '\textbf{Total}';
1344 $total->{'total_amount'} =
1345 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1346 push @total_items, $total;
1349 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1352 foreach my $credit ( $self->_items_credits ) {
1354 $total->{'total_item'} = _latex_escape($credit->{'description'});
1356 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1357 push @total_items, $total;
1361 foreach my $payment ( $self->_items_payments ) {
1363 $total->{'total_item'} = _latex_escape($payment->{'description'});
1365 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1366 push @total_items, $total;
1371 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1372 $total->{'total_amount'} =
1373 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1374 push @total_items, $total;
1378 die "guru meditation #54";
1381 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1382 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1386 ) or die "can't open temp file: $!\n";
1387 if ( $format eq 'old' ) {
1388 print $fh join('', @filled_in );
1389 } elsif ( $format eq 'Text::Template' ) {
1390 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1392 die "guru meditation #32";
1396 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1401 =item print_ps [ TIME [ , TEMPLATE ] ]
1403 Returns an postscript invoice, as a scalar.
1405 TIME an optional value used to control the printing of overdue messages. The
1406 default is now. It isn't the date of the invoice; that's the `_date' field.
1407 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1408 L<Time::Local> and L<Date::Parse> for conversion functions.
1415 my $file = $self->print_latex(@_);
1417 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1420 my $sfile = shell_quote $file;
1422 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1423 or die "pslatex $file.tex failed; see $file.log for details?\n";
1424 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1425 or die "pslatex $file.tex failed; see $file.log for details?\n";
1427 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1428 or die "dvips failed";
1430 open(POSTSCRIPT, "<$file.ps")
1431 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1433 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1436 while (<POSTSCRIPT>) {
1446 =item print_pdf [ TIME [ , TEMPLATE ] ]
1448 Returns an PDF invoice, as a scalar.
1450 TIME an optional value used to control the printing of overdue messages. The
1451 default is now. It isn't the date of the invoice; that's the `_date' field.
1452 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1453 L<Time::Local> and L<Date::Parse> for conversion functions.
1460 my $file = $self->print_latex(@_);
1462 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1465 #system('pdflatex', "$file.tex");
1466 #system('pdflatex', "$file.tex");
1467 #! LaTeX Error: Unknown graphics extension: .eps.
1469 my $sfile = shell_quote $file;
1471 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1472 or die "pslatex $file.tex failed; see $file.log for details?\n";
1473 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1474 or die "pslatex $file.tex failed; see $file.log for details?\n";
1476 #system('dvipdf', "$file.dvi", "$file.pdf" );
1478 "dvips -q -t letter -f $sfile.dvi ".
1479 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1482 or die "dvips | gs failed: $!";
1484 open(PDF, "<$file.pdf")
1485 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1487 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1500 =item print_html [ TIME [ , TEMPLATE ] ]
1502 Returns an HTML invoice, as a scalar.
1504 TIME an optional value used to control the printing of overdue messages. The
1505 default is now. It isn't the date of the invoice; that's the `_date' field.
1506 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1507 L<Time::Local> and L<Date::Parse> for conversion functions.
1514 # my $file = $self->print_latex(@_);
1516 # my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1519 # my $sfile = shell_quote $file;
1521 # system("htlatex $sfile.tex") == 0
1522 # or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1523 # #system("ltoh $sfile.tex") == 0
1524 # # or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1526 # open(HTML, "<$file.html")
1527 # or die "can't open $file.html: $! (error in LaTeX template?)\n";
1529 # #unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
1534 # s/<link\s+rel="stylesheet"\s+type="text\/css"\s+href="invoice\.(\d+)\.(\w+)\.css">/<link rel="stylesheet" type="text\/css" href="cust_bill.html?$1.$2.css">/;
1545 ##inefficient proof-of-concept for now
1546 #sub print_html_css {
1549 # my $file = $self->print_latex(@_);
1551 # my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1554 # my $sfile = shell_quote $file;
1556 # system("htlatex $sfile.tex") == 0
1557 # or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1558 # #system("ltoh $sfile.tex") == 0
1559 # # or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1561 # open(CSS, "<$file.css")
1562 # or die "can't open $file.html: $! (error in LaTeX template?)\n";
1564 # unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
1578 my( $self, $today, $template ) = @_;
1581 my $cust_main = $self->cust_main;
1582 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1583 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1585 $template ||= $self->_agent_template;
1586 my $templatefile = 'invoice_html';
1587 my $suffix = length($template) ? "_$template" : '';
1588 $templatefile .= $suffix;
1589 my @html_template = map "$_\n", $conf->config($templatefile)
1590 or die "cannot load config file $templatefile";
1592 my $html_template = new Text::Template(
1594 SOURCE => \@html_template,
1595 DELIMITERS => [ '<%=', '%>' ],
1598 $html_template->compile()
1599 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1601 my $returnaddress = $conf->exists('invoice_htmlreturnaddress')
1602 ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1603 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; }
1604 $conf->config('invoice_latexreturnaddress')
1606 warn $conf->config('invoice_latexreturnaddress');
1607 warn $returnaddress;
1609 my %invoice_data = (
1610 'invnum' => $self->invnum,
1611 'date' => time2str('%b %o, %Y', $self->_date),
1612 'agent' => encode_entities($cust_main->agent->agent),
1613 'payname' => encode_entities($cust_main->payname),
1614 'company' => encode_entities($cust_main->company),
1615 'address1' => encode_entities($cust_main->address1),
1616 'address2' => encode_entities($cust_main->address2),
1617 'city' => encode_entities($cust_main->city),
1618 'state' => encode_entities($cust_main->state),
1619 'zip' => encode_entities($cust_main->zip),
1620 # 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1621 # 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
1622 'returnaddress' => $returnaddress,
1623 'terms' => $conf->config('invoice_default_terms')
1624 || 'Payable upon receipt',
1625 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1626 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1629 my $countrydefault = $conf->config('countrydefault') || 'US';
1630 if ( $cust_main->country eq $countrydefault ) {
1631 $invoice_data{'country'} = '';
1633 $invoice_data{'country'} =
1634 encode_entities(code2country($cust_main->country));
1637 my $countrydefault = $conf->config('countrydefault') || 'US';
1638 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1640 # #do variable substitutions in notes
1641 # $invoice_data{'notes'} =
1643 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1644 # $conf->config_orbase('invoice_latexnotes', $suffix)
1647 # $invoice_data{'footer'} =~ s/\n+$//;
1648 # $invoice_data{'smallfooter'} =~ s/\n+$//;
1649 # $invoice_data{'notes'} =~ s/\n+$//;
1651 $invoice_data{'po_line'} =
1652 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1653 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1656 my $money_char = $conf->config('money_char') || '$';
1658 foreach my $line_item ( $self->_items ) {
1660 ext_description => [],
1662 $detail->{'ref'} = $line_item->{'pkgnum'};
1663 $detail->{'description'} = encode_entities($line_item->{'description'});
1664 if ( exists $line_item->{'ext_description'} ) {
1665 @{$detail->{'ext_description'}} = map {
1666 encode_entities($_);
1667 } @{$line_item->{'ext_description'}};
1669 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1670 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1672 push @{$invoice_data{'detail_items'}}, $detail;
1677 foreach my $tax ( $self->_items_tax ) {
1679 $total->{'total_item'} = encode_entities($tax->{'description'});
1680 $taxtotal += $tax->{'amount'};
1681 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1682 push @{$invoice_data{'total_items'}}, $total;
1687 $total->{'total_item'} = 'Sub-total';
1688 $total->{'total_amount'} =
1689 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1690 unshift @{$invoice_data{'total_items'}}, $total;
1693 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1696 $total->{'total_item'} = '<b>Total</b>';
1697 $total->{'total_amount'} =
1698 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1699 push @{$invoice_data{'total_items'}}, $total;
1702 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1705 foreach my $credit ( $self->_items_credits ) {
1707 $total->{'total_item'} = encode_entities($credit->{'description'});
1709 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1710 push @{$invoice_data{'total_items'}}, $total;
1714 foreach my $payment ( $self->_items_payments ) {
1716 $total->{'total_item'} = encode_entities($payment->{'description'});
1718 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1719 push @{$invoice_data{'total_items'}}, $total;
1724 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1725 $total->{'total_amount'} =
1726 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1727 push @{$invoice_data{'total_items'}}, $total;
1730 $html_template->fill_in( HASH => \%invoice_data);
1733 # quick subroutine for print_latex
1735 # There are ten characters that LaTeX treats as special characters, which
1736 # means that they do not simply typeset themselves:
1737 # # $ % & ~ _ ^ \ { }
1739 # TeX ignores blanks following an escaped character; if you want a blank (as
1740 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1744 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1745 $value =~ s/([<>])/\$$1\$/g;
1749 #utility methods for print_*
1751 sub balance_due_msg {
1753 my $msg = 'Balance Due';
1754 return $msg unless $conf->exists('invoice_default_terms');
1755 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1756 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1757 } elsif ( $conf->config('invoice_default_terms') ) {
1758 $msg .= ' - '. $conf->config('invoice_default_terms');
1765 my @display = scalar(@_)
1767 : qw( _items_previous _items_pkg );
1768 #: qw( _items_pkg );
1769 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1771 foreach my $display ( @display ) {
1772 push @b, $self->$display(@_);
1777 sub _items_previous {
1779 my $cust_main = $self->cust_main;
1780 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1782 foreach ( @pr_cust_bill ) {
1784 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1785 ' ('. time2str('%x',$_->_date). ')',
1786 #'pkgpart' => 'N/A',
1788 'amount' => sprintf("%.2f", $_->owed),
1794 # 'description' => 'Previous Balance',
1795 # #'pkgpart' => 'N/A',
1796 # 'pkgnum' => 'N/A',
1797 # 'amount' => sprintf("%10.2f", $pr_total ),
1798 # 'ext_description' => [ map {
1799 # "Invoice ". $_->invnum.
1800 # " (". time2str("%x",$_->_date). ") ".
1801 # sprintf("%10.2f", $_->owed)
1802 # } @pr_cust_bill ],
1809 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1810 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1815 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1816 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1819 sub _items_cust_bill_pkg {
1821 my $cust_bill_pkg = shift;
1824 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1826 if ( $cust_bill_pkg->pkgnum ) {
1828 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1829 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1830 my $pkg = $part_pkg->pkg;
1832 if ( $cust_bill_pkg->setup != 0 ) {
1833 my $description = $pkg;
1834 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1835 my @d = $cust_pkg->h_labels_short($self->_date);
1836 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1838 description => $description,
1839 #pkgpart => $part_pkg->pkgpart,
1840 pkgnum => $cust_pkg->pkgnum,
1841 amount => sprintf("%.2f", $cust_bill_pkg->setup),
1842 ext_description => \@d,
1846 if ( $cust_bill_pkg->recur != 0 ) {
1848 description => "$pkg (" .
1849 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1850 time2str('%x', $cust_bill_pkg->edate). ')',
1851 #pkgpart => $part_pkg->pkgpart,
1852 pkgnum => $cust_pkg->pkgnum,
1853 amount => sprintf("%.2f", $cust_bill_pkg->recur),
1854 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1855 $cust_bill_pkg->sdate),
1856 $cust_bill_pkg->details,
1861 } else { #pkgnum tax or one-shot line item (??)
1863 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1864 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1866 if ( $cust_bill_pkg->setup != 0 ) {
1868 'description' => $itemdesc,
1869 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
1872 if ( $cust_bill_pkg->recur != 0 ) {
1874 'description' => "$itemdesc (".
1875 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1876 time2str("%x", $cust_bill_pkg->edate). ')',
1877 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
1889 sub _items_credits {
1894 foreach ( $self->cust_credited ) {
1896 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1898 my $reason = $_->cust_credit->reason;
1899 #my $reason = substr($_->cust_credit->reason,0,32);
1900 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1901 $reason = " ($reason) " if $reason;
1903 #'description' => 'Credit ref\#'. $_->crednum.
1904 # " (". time2str("%x",$_->cust_credit->_date) .")".
1906 'description' => 'Credit applied '.
1907 time2str("%x",$_->cust_credit->_date). $reason,
1908 'amount' => sprintf("%.2f",$_->amount),
1911 #foreach ( @cr_cust_credit ) {
1913 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1914 # $money_char. sprintf("%10.2f",$_->credited)
1922 sub _items_payments {
1926 #get & print payments
1927 foreach ( $self->cust_bill_pay ) {
1929 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1932 'description' => "Payment received ".
1933 time2str("%x",$_->cust_pay->_date ),
1934 'amount' => sprintf("%.2f", $_->amount )
1948 print_text formatting (and some logic :/) is in source, but needs to be
1949 slurped in from a file. Also number of lines ($=).
1953 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1954 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base