4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
8 use Text::Template 1.20;
10 use String::ShellQuote;
13 use FS::UID qw( datasrc );
14 use FS::Misc qw( send_email send_fax );
15 use FS::Record qw( qsearch qsearchs );
16 use FS::cust_main_Mixin;
18 use FS::cust_bill_pkg;
22 use FS::cust_credit_bill;
23 use FS::cust_pay_batch;
24 use FS::cust_bill_event;
26 use FS::cust_bill_pay;
27 use FS::part_bill_event;
29 @ISA = qw( FS::cust_main_Mixin FS::Record );
33 #ask FS::UID to run this stuff for us later
34 FS::UID->install_callback( sub {
36 $money_char = $conf->config('money_char') || '$';
41 FS::cust_bill - Object methods for cust_bill records
47 $record = new FS::cust_bill \%hash;
48 $record = new FS::cust_bill { 'column' => 'value' };
50 $error = $record->insert;
52 $error = $new_record->replace($old_record);
54 $error = $record->delete;
56 $error = $record->check;
58 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
60 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
62 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
64 @cust_pay_objects = $cust_bill->cust_pay;
66 $tax_amount = $record->tax;
68 @lines = $cust_bill->print_text;
69 @lines = $cust_bill->print_text $time;
73 An FS::cust_bill object represents an invoice; a declaration that a customer
74 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
75 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
76 following fields are currently supported:
80 =item invnum - primary key (assigned automatically for new invoices)
82 =item custnum - customer (see L<FS::cust_main>)
84 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
85 L<Time::Local> and L<Date::Parse> for conversion functions.
87 =item charged - amount of this invoice
89 =item printed - deprecated
91 =item closed - books closed flag, empty or `Y'
101 Creates a new invoice. To add the invoice to the database, see L<"insert">.
102 Invoices are normally created by calling the bill method of a customer object
103 (see L<FS::cust_main>).
107 sub table { 'cust_bill'; }
109 sub cust_linked { $_[0]->cust_main_custnum; }
110 sub cust_unlinked_msg {
112 "WARNING: can't find cust_main.custnum ". $self->custnum.
113 ' (cust_bill.invnum '. $self->invnum. ')';
118 Adds this invoice to the database ("Posts" the invoice). If there is an error,
119 returns the error, otherwise returns false.
123 Currently unimplemented. I don't remove invoices because there would then be
124 no record you ever posted this invoice (which is bad, no?)
130 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
131 $self->SUPER::delete(@_);
134 =item replace OLD_RECORD
136 Replaces the OLD_RECORD with this one in the database. If there is an error,
137 returns the error, otherwise returns false.
139 Only printed may be changed. printed is normally updated by calling the
140 collect method of a customer object (see L<FS::cust_main>).
145 my( $new, $old ) = ( shift, shift );
146 return "Can't change custnum!" unless $old->custnum == $new->custnum;
147 #return "Can't change _date!" unless $old->_date eq $new->_date;
148 return "Can't change _date!" unless $old->_date == $new->_date;
149 return "Can't change charged!" unless $old->charged == $new->charged;
151 $new->SUPER::replace($old);
156 Checks all fields to make sure this is a valid invoice. If there is an error,
157 returns the error, otherwise returns false. Called by the insert and replace
166 $self->ut_numbern('invnum')
167 || $self->ut_number('custnum')
168 || $self->ut_numbern('_date')
169 || $self->ut_money('charged')
170 || $self->ut_numbern('printed')
171 || $self->ut_enum('closed', [ '', 'Y' ])
173 return $error if $error;
175 return "Unknown customer"
176 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
178 $self->_date(time) unless $self->_date;
180 $self->printed(0) if $self->printed eq '';
187 Returns a list consisting of the total previous balance for this customer,
188 followed by the previous outstanding invoices (as FS::cust_bill objects also).
195 my @cust_bill = sort { $a->_date <=> $b->_date }
196 grep { $_->owed != 0 && $_->_date < $self->_date }
197 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
199 foreach ( @cust_bill ) { $total += $_->owed; }
205 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
211 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
214 =item cust_bill_event
216 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
221 sub cust_bill_event {
223 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
229 Returns the customer (see L<FS::cust_main>) for this invoice.
235 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
240 Depreciated. See the cust_credited method.
242 #Returns a list consisting of the total previous credited (see
243 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
244 #outstanding credits (FS::cust_credit objects).
250 croak "FS::cust_bill->cust_credit depreciated; see ".
251 "FS::cust_bill->cust_credit_bill";
254 #my @cust_credit = sort { $a->_date <=> $b->_date }
255 # grep { $_->credited != 0 && $_->_date < $self->_date }
256 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
258 #foreach (@cust_credit) { $total += $_->credited; }
259 #$total, @cust_credit;
264 Depreciated. See the cust_bill_pay method.
266 #Returns all payments (see L<FS::cust_pay>) for this invoice.
272 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
274 #sort { $a->_date <=> $b->_date }
275 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
281 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
287 sort { $a->_date <=> $b->_date }
288 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
293 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
299 sort { $a->_date <=> $b->_date }
300 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
306 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
313 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
315 foreach (@taxlines) { $total += $_->setup; }
321 Returns the amount owed (still outstanding) on this invoice, which is charged
322 minus all payment applications (see L<FS::cust_bill_pay>) and credit
323 applications (see L<FS::cust_credit_bill>).
329 my $balance = $self->charged;
330 $balance -= $_->amount foreach ( $self->cust_bill_pay );
331 $balance -= $_->amount foreach ( $self->cust_credited );
332 $balance = sprintf( "%.2f", $balance);
333 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
338 =item generate_email PARAMHASH
340 PARAMHASH can contain the following:
344 =item from => sender address, required
346 =item tempate => alternate template name, optional
348 =item print_text => text attachment arrayref, optional
350 =item subject => email subject, optional
354 Returns an argument list to be passed to L<FS::Misc::send_email>.
365 my $me = '[FS::cust_bill::generate_email]';
368 'from' => $args{'from'},
369 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
372 if (ref($args{'to'} eq 'ARRAY')) {
373 $return{'to'} = $args{'to'};
375 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
376 $self->cust_main->invoicing_list
380 if ( $conf->exists('invoice_html') ) {
382 warn "$me creating HTML/text multipart message"
385 $return{'nobody'} = 1;
387 my $alternative = build MIME::Entity
388 'Type' => 'multipart/alternative',
389 'Encoding' => '7bit',
390 'Disposition' => 'inline'
394 if ( $conf->exists('invoice_email_pdf')
395 and scalar($conf->config('invoice_email_pdf_note')) ) {
397 warn "$me using 'invoice_email_pdf_note' in multipart message"
399 $data = [ map { $_ . "\n" }
400 $conf->config('invoice_email_pdf_note')
405 warn "$me not using 'invoice_email_pdf_note' in multipart message"
407 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
408 $data = $args{'print_text'};
410 $data = [ $self->print_text('', $args{'template'}) ];
415 $alternative->attach(
416 'Type' => 'text/plain',
417 #'Encoding' => 'quoted-printable',
418 'Encoding' => '7bit',
420 'Disposition' => 'inline',
423 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
424 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
426 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
428 if ( defined($args{'_template'}) && length($args{'_template'})
429 && -e "$path/logo_". $args{'_template'}. ".png"
432 $file = "$path/logo_". $args{'_template'}. ".png";
434 $file = "$path/logo.png";
437 my $image = build MIME::Entity
438 'Type' => 'image/png',
439 'Encoding' => 'base64',
441 'Filename' => 'logo.png',
442 'Content-ID' => "<$content_id>",
445 $alternative->attach(
446 'Type' => 'text/html',
447 'Encoding' => 'quoted-printable',
448 'Data' => [ '<html>',
451 ' '. encode_entities($return{'subject'}),
454 ' <body bgcolor="#e8e8e8">',
455 $self->print_html('', $args{'template'}, $content_id),
459 'Disposition' => 'inline',
460 #'Filename' => 'invoice.pdf',
463 if ( $conf->exists('invoice_email_pdf') ) {
468 # multipart/alternative
474 my $related = build MIME::Entity 'Type' => 'multipart/related',
475 'Encoding' => '7bit';
477 #false laziness w/Misc::send_email
478 $related->head->replace('Content-type',
480 '; boundary="'. $related->head->multipart_boundary. '"'.
481 '; type=multipart/alternative'
484 $related->add_part($alternative);
486 $related->add_part($image);
488 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
490 $return{'mimeparts'} = [ $related, $pdf ];
494 #no other attachment:
496 # multipart/alternative
501 $return{'content-type'} = 'multipart/related';
502 $return{'mimeparts'} = [ $alternative, $image ];
503 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
504 #$return{'disposition'} = 'inline';
510 if ( $conf->exists('invoice_email_pdf') ) {
511 warn "$me creating PDF attachment"
514 #mime parts arguments a la MIME::Entity->build().
515 $return{'mimeparts'} = [
516 { $self->mimebuild_pdf('', $args{'template'}) }
520 if ( $conf->exists('invoice_email_pdf')
521 and scalar($conf->config('invoice_email_pdf_note')) ) {
523 warn "$me using 'invoice_email_pdf_note'"
525 $return{'body'} = [ map { $_ . "\n" }
526 $conf->config('invoice_email_pdf_note')
531 warn "$me not using 'invoice_email_pdf_note'"
533 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
534 $return{'body'} = $args{'print_text'};
536 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
549 Returns a list suitable for passing to MIME::Entity->build(), representing
550 this invoice as PDF attachment.
557 'Type' => 'application/pdf',
558 'Encoding' => 'base64',
559 'Data' => [ $self->print_pdf(@_) ],
560 'Disposition' => 'attachment',
561 'Filename' => 'invoice.pdf',
565 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
567 Sends this invoice to the destinations configured for this customer: sends
568 email, prints and/or faxes. See L<FS::cust_main_invoice>.
570 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
572 AGENTNUM, if specified, means that this invoice will only be sent for customers
573 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
574 single agent) or an arrayref of agentnums.
576 INVOICE_FROM, if specified, overrides the default email invoice From: address.
582 my $template = scalar(@_) ? shift : '';
583 if ( scalar(@_) && $_[0] ) {
584 my $agentnums = ref($_[0]) ? shift : [ shift ];
585 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
591 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
593 my @invoicing_list = $self->cust_main->invoicing_list;
595 $self->email($template, $invoice_from)
596 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
598 $self->print($template)
599 if grep { $_ eq 'POST' } @invoicing_list; #postal
601 $self->fax($template)
602 if grep { $_ eq 'FAX' } @invoicing_list; #fax
608 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
612 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
614 INVOICE_FROM, if specified, overrides the default email invoice From: address.
620 my $template = scalar(@_) ? shift : '';
624 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
626 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
627 $self->cust_main->invoicing_list;
629 #better to notify this person than silence
630 @invoicing_list = ($invoice_from) unless @invoicing_list;
632 my $error = send_email(
633 $self->generate_email(
634 'from' => $invoice_from,
635 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
636 'template' => $template,
639 die "can't email invoice: $error\n" if $error;
640 #die "$error\n" if $error;
644 =item lpr_data [ TEMPLATENAME ]
646 Returns the postscript or plaintext for this invoice as an arrayref.
648 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
653 my( $self, $template) = @_;
654 $conf->exists('invoice_latex')
655 ? [ $self->print_ps('', $template) ]
656 : [ $self->print_text('', $template) ];
659 =item print [ TEMPLATENAME ]
663 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
669 my $template = scalar(@_) ? shift : '';
671 my $lpr = $conf->config('lpr');
674 run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
676 $outerr = ": $outerr" if length($outerr);
677 die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
682 =item fax [ TEMPLATENAME ]
686 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
692 my $template = scalar(@_) ? shift : '';
694 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
695 unless $conf->exists('invoice_latex');
697 my $dialstring = $self->cust_main->getfield('fax');
700 my $error = send_fax( 'docdata' => $self->lpr_data($template),
701 'dialstring' => $dialstring,
703 die $error if $error;
707 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
709 Like B<send>, but only sends the invoice if it is the newest open invoice for
719 grep { $_->owed > 0 }
720 qsearch('cust_bill', {
721 'custnum' => $self->custnum,
722 #'_date' => { op=>'>', value=>$self->_date },
723 'invnum' => { op=>'>', value=>$self->invnum },
730 =item send_csv OPTIONS
732 Sends invoice as a CSV data-file to a remote host with the specified protocol.
736 protocol - currently only "ftp"
742 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
743 and YYMMDDHHMMSS is a timestamp.
745 The fields of the CSV file is as follows:
747 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
751 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
753 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
754 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
755 fields are filled in.
757 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
758 first two fields (B<record_type> and B<invnum>) and the last five fields
759 (B<pkg> through B<edate>) are filled in.
761 =item invnum - invoice number
763 =item custnum - customer number
765 =item _date - invoice date
767 =item charged - total invoice amount
769 =item first - customer first name
771 =item last - customer first name
773 =item company - company name
775 =item address1 - address line 1
777 =item address2 - address line 1
787 =item pkg - line item description
789 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
791 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
793 =item sdate - start date for recurring fee
795 =item edate - end date for recurring fee
802 my($self, %opt) = @_;
804 #part one: create file
806 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
807 mkdir $spooldir, 0700 unless -d $spooldir;
809 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
811 open(CSV, ">$file") or die "can't open $file: $!";
813 eval "use Text::CSV_XS";
816 my $csv = Text::CSV_XS->new({'always_quote'=>1});
818 my $cust_main = $self->cust_main;
824 time2str("%x", $self->_date),
825 sprintf("%.2f", $self->charged),
826 ( map { $cust_main->getfield($_) }
827 qw( first last company address1 address2 city state zip country ) ),
829 ) or die "can't create csv";
830 print CSV $csv->string. "\n";
832 #new charges (false laziness w/print_text and _items stuff)
833 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
835 my($pkg, $setup, $recur, $sdate, $edate);
836 if ( $cust_bill_pkg->pkgnum ) {
838 ($pkg, $setup, $recur, $sdate, $edate) = (
839 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
840 ( $cust_bill_pkg->setup != 0
841 ? sprintf("%.2f", $cust_bill_pkg->setup )
843 ( $cust_bill_pkg->recur != 0
844 ? sprintf("%.2f", $cust_bill_pkg->recur )
846 time2str("%x", $cust_bill_pkg->sdate),
847 time2str("%x", $cust_bill_pkg->edate),
851 next unless $cust_bill_pkg->setup != 0;
852 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
853 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
855 ($pkg, $setup, $recur, $sdate, $edate) =
856 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
862 ( map { '' } (1..11) ),
863 ($pkg, $setup, $recur, $sdate, $edate)
864 ) or die "can't create csv";
865 print CSV $csv->string. "\n";
869 close CSV or die "can't close CSV: $!";
874 if ( $opt{protocol} eq 'ftp' ) {
875 eval "use Net::FTP;";
877 $net = Net::FTP->new($opt{server}) or die @$;
879 die "unknown protocol: $opt{protocol}";
882 $net->login( $opt{username}, $opt{password} )
883 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
885 $net->binary or die "can't set binary mode";
887 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
889 $net->put($file) or die "can't put $file: $!";
899 Pays this invoice with a compliemntary payment. If there is an error,
900 returns the error, otherwise returns false.
906 my $cust_pay = new FS::cust_pay ( {
907 'invnum' => $self->invnum,
908 'paid' => $self->owed,
911 'payinfo' => $self->cust_main->payinfo,
919 Attempts to pay this invoice with a credit card payment via a
920 Business::OnlinePayment realtime gateway. See
921 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
922 for supported processors.
928 $self->realtime_bop( 'CC', @_ );
933 Attempts to pay this invoice with an electronic check (ACH) payment via a
934 Business::OnlinePayment realtime gateway. See
935 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
936 for supported processors.
942 $self->realtime_bop( 'ECHECK', @_ );
947 Attempts to pay this invoice with phone bill (LEC) payment via a
948 Business::OnlinePayment realtime gateway. See
949 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
950 for supported processors.
956 $self->realtime_bop( 'LEC', @_ );
960 my( $self, $method ) = @_;
962 my $cust_main = $self->cust_main;
963 my $balance = $cust_main->balance;
964 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
965 $amount = sprintf("%.2f", $amount);
966 return "not run (balance $balance)" unless $amount > 0;
968 my $description = 'Internet Services';
969 if ( $conf->exists('business-onlinepayment-description') ) {
970 my $dtempl = $conf->config('business-onlinepayment-description');
972 my $agent_obj = $cust_main->agent
973 or die "can't retreive agent for $cust_main (agentnum ".
974 $cust_main->agentnum. ")";
975 my $agent = $agent_obj->agent;
976 my $pkgs = join(', ',
977 map { $_->cust_pkg->part_pkg->pkg }
978 grep { $_->pkgnum } $self->cust_bill_pkg
980 $description = eval qq("$dtempl");
983 $cust_main->realtime_bop($method, $amount,
984 'description' => $description,
985 'invnum' => $self->invnum,
992 Adds a payment for this invoice to the pending credit card batch (see
993 L<FS::cust_pay_batch>).
999 my $cust_main = $self->cust_main;
1001 my $cust_pay_batch = new FS::cust_pay_batch ( {
1002 'invnum' => $self->getfield('invnum'),
1003 'custnum' => $cust_main->getfield('custnum'),
1004 'last' => $cust_main->getfield('last'),
1005 'first' => $cust_main->getfield('first'),
1006 'address1' => $cust_main->getfield('address1'),
1007 'address2' => $cust_main->getfield('address2'),
1008 'city' => $cust_main->getfield('city'),
1009 'state' => $cust_main->getfield('state'),
1010 'zip' => $cust_main->getfield('zip'),
1011 'country' => $cust_main->getfield('country'),
1012 'cardnum' => $cust_main->payinfo,
1013 'exp' => $cust_main->getfield('paydate'),
1014 'payname' => $cust_main->getfield('payname'),
1015 'amount' => $self->owed,
1017 my $error = $cust_pay_batch->insert;
1018 die $error if $error;
1023 sub _agent_template {
1025 $self->_agent_plandata('agent_templatename');
1028 sub _agent_invoice_from {
1030 $self->_agent_plandata('agent_invoice_from');
1033 sub _agent_plandata {
1034 my( $self, $option ) = @_;
1036 my $part_bill_event = qsearchs( 'part_bill_event',
1038 'payby' => $self->cust_main->payby,
1039 'plan' => 'send_agent',
1040 'plandata' => { 'op' => '~',
1041 'value' => "(^|\n)agentnum ".
1043 $self->cust_main->agentnum.
1049 'ORDER BY seconds LIMIT 1'
1052 return '' unless $part_bill_event;
1054 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1057 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1058 " plandata for $option";
1064 =item print_text [ TIME [ , TEMPLATE ] ]
1066 Returns an text invoice, as a list of lines.
1068 TIME an optional value used to control the printing of overdue messages. The
1069 default is now. It isn't the date of the invoice; that's the `_date' field.
1070 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1071 L<Time::Local> and L<Date::Parse> for conversion functions.
1075 #still some false laziness w/_items stuff (and send_csv)
1078 my( $self, $today, $template ) = @_;
1081 # my $invnum = $self->invnum;
1082 my $cust_main = $self->cust_main;
1083 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1084 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1086 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1087 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1088 #my $balance_due = $self->owed + $pr_total - $cr_total;
1089 my $balance_due = $self->owed + $pr_total;
1092 #my($description,$amount);
1096 foreach ( @pr_cust_bill ) {
1098 "Previous Balance, Invoice #". $_->invnum.
1099 " (". time2str("%x",$_->_date). ")",
1100 $money_char. sprintf("%10.2f",$_->owed)
1103 if (@pr_cust_bill) {
1104 push @buf,['','-----------'];
1105 push @buf,[ 'Total Previous Balance',
1106 $money_char. sprintf("%10.2f",$pr_total ) ];
1111 foreach my $cust_bill_pkg (
1112 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1113 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1116 my $desc = $cust_bill_pkg->desc;
1118 if ( $cust_bill_pkg->pkgnum > 0 ) {
1120 if ( $cust_bill_pkg->setup != 0 ) {
1121 my $description = $desc;
1122 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1123 push @buf, [ $description,
1124 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1126 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1127 $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1130 if ( $cust_bill_pkg->recur != 0 ) {
1132 "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1133 time2str("%x", $cust_bill_pkg->edate) . ")",
1134 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1137 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1138 $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1139 $cust_bill_pkg->sdate );
1142 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1144 } else { #pkgnum tax or one-shot line item
1146 if ( $cust_bill_pkg->setup != 0 ) {
1148 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1150 if ( $cust_bill_pkg->recur != 0 ) {
1151 push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1152 . time2str("%x", $cust_bill_pkg->edate). ")",
1153 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1161 push @buf,['','-----------'];
1162 push @buf,['Total New Charges',
1163 $money_char. sprintf("%10.2f",$self->charged) ];
1166 push @buf,['','-----------'];
1167 push @buf,['Total Charges',
1168 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1172 foreach ( $self->cust_credited ) {
1174 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1176 my $reason = substr($_->cust_credit->reason,0,32);
1177 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1178 $reason = " ($reason) " if $reason;
1180 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1182 $money_char. sprintf("%10.2f",$_->amount)
1185 #foreach ( @cr_cust_credit ) {
1187 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1188 # $money_char. sprintf("%10.2f",$_->credited)
1192 #get & print payments
1193 foreach ( $self->cust_bill_pay ) {
1195 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1198 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1199 $money_char. sprintf("%10.2f",$_->amount )
1204 my $balance_due_msg = $self->balance_due_msg;
1206 push @buf,['','-----------'];
1207 push @buf,[$balance_due_msg, $money_char.
1208 sprintf("%10.2f", $balance_due ) ];
1210 #create the template
1211 $template ||= $self->_agent_template;
1212 my $templatefile = 'invoice_template';
1213 $templatefile .= "_$template" if length($template);
1214 my @invoice_template = $conf->config($templatefile)
1215 or die "cannot load config file $templatefile";
1218 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1219 /invoice_lines\((\d*)\)/;
1220 $invoice_lines += $1 || scalar(@buf);
1223 die "no invoice_lines() functions in template?" unless $wasfunc;
1224 my $invoice_template = new Text::Template (
1226 SOURCE => [ map "$_\n", @invoice_template ],
1227 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1228 $invoice_template->compile()
1229 or die "can't compile template: $Text::Template::ERROR";
1231 #setup template variables
1232 package FS::cust_bill::_template; #!
1233 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1235 $invnum = $self->invnum;
1236 $date = $self->_date;
1238 $agent = $self->cust_main->agent->agent;
1240 if ( $FS::cust_bill::invoice_lines ) {
1242 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1244 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1249 #format address (variable for the template)
1251 @address = ( '', '', '', '', '', '' );
1252 package FS::cust_bill; #!
1253 $FS::cust_bill::_template::address[$l++] =
1254 $cust_main->payname.
1255 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1256 ? " (P.O. #". $cust_main->payinfo. ")"
1260 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1261 if $cust_main->company;
1262 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1263 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1264 if $cust_main->address2;
1265 $FS::cust_bill::_template::address[$l++] =
1266 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1268 my $countrydefault = $conf->config('countrydefault') || 'US';
1269 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1270 unless $cust_main->country eq $countrydefault;
1272 # #overdue? (variable for the template)
1273 # $FS::cust_bill::_template::overdue = (
1275 # && $today > $self->_date
1276 ## && $self->printed > 1
1277 # && $self->printed > 0
1280 #and subroutine for the template
1281 sub FS::cust_bill::_template::invoice_lines {
1282 my $lines = shift || scalar(@buf);
1284 scalar(@buf) ? shift @buf : [ '', '' ];
1290 $FS::cust_bill::_template::page = 1;
1294 push @collect, split("\n",
1295 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1297 $FS::cust_bill::_template::page++;
1300 map "$_\n", @collect;
1304 =item print_latex [ TIME [ , TEMPLATE ] ]
1306 Internal method - returns a filename of a filled-in LaTeX template for this
1307 invoice (Note: add ".tex" to get the actual filename).
1309 See print_ps and print_pdf for methods that return PostScript and PDF output.
1311 TIME an optional value used to control the printing of overdue messages. The
1312 default is now. It isn't the date of the invoice; that's the `_date' field.
1313 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1314 L<Time::Local> and L<Date::Parse> for conversion functions.
1318 #still some false laziness w/print_text (mostly print_text should use _items stuff though)
1321 my( $self, $today, $template ) = @_;
1323 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1326 my $cust_main = $self->cust_main;
1327 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1328 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1330 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1331 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1332 #my $balance_due = $self->owed + $pr_total - $cr_total;
1333 my $balance_due = $self->owed + $pr_total;
1335 #create the template
1336 $template ||= $self->_agent_template;
1337 my $templatefile = 'invoice_latex';
1338 my $suffix = length($template) ? "_$template" : '';
1339 $templatefile .= $suffix;
1340 my @invoice_template = map "$_\n", $conf->config($templatefile)
1341 or die "cannot load config file $templatefile";
1343 my($format, $text_template);
1344 if ( grep { /^%%Detail/ } @invoice_template ) {
1345 #change this to a die when the old code is removed
1346 warn "old-style invoice template $templatefile; ".
1347 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1350 $format = 'Text::Template';
1351 $text_template = new Text::Template(
1353 SOURCE => \@invoice_template,
1354 DELIMITERS => [ '[@--', '--@]' ],
1357 $text_template->compile()
1358 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1362 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1363 $returnaddress = join("\n",
1364 $conf->config_orbase('invoice_latexreturnaddress', $template)
1367 $returnaddress = '~';
1370 my %invoice_data = (
1371 'invnum' => $self->invnum,
1372 'date' => time2str('%b %o, %Y', $self->_date),
1373 'today' => time2str('%b %o, %Y', $today),
1374 'agent' => _latex_escape($cust_main->agent->agent),
1375 'payname' => _latex_escape($cust_main->payname),
1376 'company' => _latex_escape($cust_main->company),
1377 'address1' => _latex_escape($cust_main->address1),
1378 'address2' => _latex_escape($cust_main->address2),
1379 'city' => _latex_escape($cust_main->city),
1380 'state' => _latex_escape($cust_main->state),
1381 'zip' => _latex_escape($cust_main->zip),
1382 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1383 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1384 'returnaddress' => $returnaddress,
1386 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1387 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1388 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1391 my $countrydefault = $conf->config('countrydefault') || 'US';
1392 if ( $cust_main->country eq $countrydefault ) {
1393 $invoice_data{'country'} = '';
1395 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1398 $invoice_data{'notes'} =
1400 # #do variable substitutions in notes
1401 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1402 $conf->config_orbase('invoice_latexnotes', $template)
1404 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1407 $invoice_data{'footer'} =~ s/\n+$//;
1408 $invoice_data{'smallfooter'} =~ s/\n+$//;
1409 $invoice_data{'notes'} =~ s/\n+$//;
1411 $invoice_data{'po_line'} =
1412 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1413 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1417 if ( $format eq 'old' ) {
1420 my @total_item = ();
1421 while ( @invoice_template ) {
1422 my $line = shift @invoice_template;
1424 if ( $line =~ /^%%Detail\s*$/ ) {
1426 while ( ( my $line_item_line = shift @invoice_template )
1427 !~ /^%%EndDetail\s*$/ ) {
1428 push @line_item, $line_item_line;
1430 foreach my $line_item ( $self->_items ) {
1431 #foreach my $line_item ( $self->_items_pkg ) {
1432 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1433 $invoice_data{'description'} =
1434 _latex_escape($line_item->{'description'});
1435 if ( exists $line_item->{'ext_description'} ) {
1436 $invoice_data{'description'} .=
1437 "\\tabularnewline\n~~".
1438 join( "\\tabularnewline\n~~",
1439 map _latex_escape($_), @{$line_item->{'ext_description'}}
1442 $invoice_data{'amount'} = $line_item->{'amount'};
1443 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1445 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1448 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1450 while ( ( my $total_item_line = shift @invoice_template )
1451 !~ /^%%EndTotalDetails\s*$/ ) {
1452 push @total_item, $total_item_line;
1455 my @total_fill = ();
1458 foreach my $tax ( $self->_items_tax ) {
1459 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1460 $taxtotal += $tax->{'amount'};
1461 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1463 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1468 $invoice_data{'total_item'} = 'Sub-total';
1469 $invoice_data{'total_amount'} =
1470 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1471 unshift @total_fill,
1472 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1476 $invoice_data{'total_item'} = '\textbf{Total}';
1477 $invoice_data{'total_amount'} =
1478 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1480 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1483 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1486 foreach my $credit ( $self->_items_credits ) {
1487 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1489 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1491 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1496 foreach my $payment ( $self->_items_payments ) {
1497 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1499 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1501 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1505 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1506 $invoice_data{'total_amount'} =
1507 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1509 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1512 push @filled_in, @total_fill;
1515 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1516 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1517 push @filled_in, $line;
1528 } elsif ( $format eq 'Text::Template' ) {
1530 my @detail_items = ();
1531 my @total_items = ();
1533 $invoice_data{'detail_items'} = \@detail_items;
1534 $invoice_data{'total_items'} = \@total_items;
1536 foreach my $line_item ( $self->_items ) {
1538 ext_description => [],
1540 $detail->{'ref'} = $line_item->{'pkgnum'};
1541 $detail->{'quantity'} = 1;
1542 $detail->{'description'} = _latex_escape($line_item->{'description'});
1543 if ( exists $line_item->{'ext_description'} ) {
1544 @{$detail->{'ext_description'}} = map {
1546 } @{$line_item->{'ext_description'}};
1548 $detail->{'amount'} = $line_item->{'amount'};
1549 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1551 push @detail_items, $detail;
1556 foreach my $tax ( $self->_items_tax ) {
1558 $total->{'total_item'} = _latex_escape($tax->{'description'});
1559 $taxtotal += $tax->{'amount'};
1560 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1561 push @total_items, $total;
1566 $total->{'total_item'} = 'Sub-total';
1567 $total->{'total_amount'} =
1568 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1569 unshift @total_items, $total;
1574 $total->{'total_item'} = '\textbf{Total}';
1575 $total->{'total_amount'} =
1576 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1577 push @total_items, $total;
1580 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1583 foreach my $credit ( $self->_items_credits ) {
1585 $total->{'total_item'} = _latex_escape($credit->{'description'});
1587 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1588 push @total_items, $total;
1592 foreach my $payment ( $self->_items_payments ) {
1594 $total->{'total_item'} = _latex_escape($payment->{'description'});
1596 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1597 push @total_items, $total;
1602 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1603 $total->{'total_amount'} =
1604 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1605 push @total_items, $total;
1609 die "guru meditation #54";
1612 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1613 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1617 ) or die "can't open temp file: $!\n";
1618 if ( $format eq 'old' ) {
1619 print $fh join('', @filled_in );
1620 } elsif ( $format eq 'Text::Template' ) {
1621 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1623 die "guru meditation #32";
1627 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1632 =item print_ps [ TIME [ , TEMPLATE ] ]
1634 Returns an postscript invoice, as a scalar.
1636 TIME an optional value used to control the printing of overdue messages. The
1637 default is now. It isn't the date of the invoice; that's the `_date' field.
1638 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1639 L<Time::Local> and L<Date::Parse> for conversion functions.
1646 my $file = $self->print_latex(@_);
1648 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1651 my $sfile = shell_quote $file;
1653 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1654 or die "pslatex $file.tex failed; see $file.log for details?\n";
1655 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1656 or die "pslatex $file.tex failed; see $file.log for details?\n";
1658 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1659 or die "dvips failed";
1661 open(POSTSCRIPT, "<$file.ps")
1662 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1664 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1667 while (<POSTSCRIPT>) {
1677 =item print_pdf [ TIME [ , TEMPLATE ] ]
1679 Returns an PDF invoice, as a scalar.
1681 TIME an optional value used to control the printing of overdue messages. The
1682 default is now. It isn't the date of the invoice; that's the `_date' field.
1683 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1684 L<Time::Local> and L<Date::Parse> for conversion functions.
1691 my $file = $self->print_latex(@_);
1693 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1696 #system('pdflatex', "$file.tex");
1697 #system('pdflatex', "$file.tex");
1698 #! LaTeX Error: Unknown graphics extension: .eps.
1700 my $sfile = shell_quote $file;
1702 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1703 or die "pslatex $file.tex failed; see $file.log for details?\n";
1704 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1705 or die "pslatex $file.tex failed; see $file.log for details?\n";
1707 #system('dvipdf', "$file.dvi", "$file.pdf" );
1709 "dvips -q -t letter -f $sfile.dvi ".
1710 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1713 or die "dvips | gs failed: $!";
1715 open(PDF, "<$file.pdf")
1716 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1718 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1731 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1733 Returns an HTML invoice, as a scalar.
1735 TIME an optional value used to control the printing of overdue messages. The
1736 default is now. It isn't the date of the invoice; that's the `_date' field.
1737 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1738 L<Time::Local> and L<Date::Parse> for conversion functions.
1740 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1741 when emailing the invoice as part of a multipart/related MIME email.
1746 my( $self, $today, $template, $cid ) = @_;
1749 my $cust_main = $self->cust_main;
1750 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1751 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1753 $template ||= $self->_agent_template;
1754 my $templatefile = 'invoice_html';
1755 my $suffix = length($template) ? "_$template" : '';
1756 $templatefile .= $suffix;
1757 my @html_template = map "$_\n", $conf->config($templatefile)
1758 or die "cannot load config file $templatefile";
1760 my $html_template = new Text::Template(
1762 SOURCE => \@html_template,
1763 DELIMITERS => [ '<%=', '%>' ],
1766 $html_template->compile()
1767 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1769 my %invoice_data = (
1770 'invnum' => $self->invnum,
1771 'date' => time2str('%b %o, %Y', $self->_date),
1772 'today' => time2str('%b %o, %Y', $today),
1773 'agent' => encode_entities($cust_main->agent->agent),
1774 'payname' => encode_entities($cust_main->payname),
1775 'company' => encode_entities($cust_main->company),
1776 'address1' => encode_entities($cust_main->address1),
1777 'address2' => encode_entities($cust_main->address2),
1778 'city' => encode_entities($cust_main->city),
1779 'state' => encode_entities($cust_main->state),
1780 'zip' => encode_entities($cust_main->zip),
1781 'terms' => $conf->config('invoice_default_terms')
1782 || 'Payable upon receipt',
1784 'template' => $template,
1785 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1789 defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1790 && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1792 $invoice_data{'returnaddress'} =
1793 join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
1795 $invoice_data{'returnaddress'} =
1798 s/\\\\\*?\s*$/<BR>/;
1799 s/\\hyphenation\{[\w\s\-]+\}//;
1802 $conf->config_orbase( 'invoice_latexreturnaddress',
1808 my $countrydefault = $conf->config('countrydefault') || 'US';
1809 if ( $cust_main->country eq $countrydefault ) {
1810 $invoice_data{'country'} = '';
1812 $invoice_data{'country'} =
1813 encode_entities(code2country($cust_main->country));
1817 defined( $conf->config_orbase('invoice_htmlnotes', $template) )
1818 && length( $conf->config_orbase('invoice_htmlnotes', $template) )
1820 $invoice_data{'notes'} =
1821 join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
1823 $invoice_data{'notes'} =
1825 s/%%(.*)$/<!-- $1 -->/;
1826 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1827 s/\\begin\{enumerate\}/<ol>/;
1829 s/\\end\{enumerate\}/<\/ol>/;
1830 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1833 $conf->config_orbase('invoice_latexnotes', $template)
1837 # #do variable substitutions in notes
1838 # $invoice_data{'notes'} =
1840 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1841 # $conf->config_orbase('invoice_latexnotes', $suffix)
1845 defined( $conf->config_orbase('invoice_htmlfooter', $template) )
1846 && length( $conf->config_orbase('invoice_htmlfooter', $template) )
1848 $invoice_data{'footer'} =
1849 join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
1851 $invoice_data{'footer'} =
1852 join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1853 $conf->config_orbase('invoice_latexfooter', $template)
1857 $invoice_data{'po_line'} =
1858 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1859 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1862 my $money_char = $conf->config('money_char') || '$';
1864 foreach my $line_item ( $self->_items ) {
1866 ext_description => [],
1868 $detail->{'ref'} = $line_item->{'pkgnum'};
1869 $detail->{'description'} = encode_entities($line_item->{'description'});
1870 if ( exists $line_item->{'ext_description'} ) {
1871 @{$detail->{'ext_description'}} = map {
1872 encode_entities($_);
1873 } @{$line_item->{'ext_description'}};
1875 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1876 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1878 push @{$invoice_data{'detail_items'}}, $detail;
1883 foreach my $tax ( $self->_items_tax ) {
1885 $total->{'total_item'} = encode_entities($tax->{'description'});
1886 $taxtotal += $tax->{'amount'};
1887 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1888 push @{$invoice_data{'total_items'}}, $total;
1893 $total->{'total_item'} = 'Sub-total';
1894 $total->{'total_amount'} =
1895 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1896 unshift @{$invoice_data{'total_items'}}, $total;
1899 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1902 $total->{'total_item'} = '<b>Total</b>';
1903 $total->{'total_amount'} =
1904 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1905 push @{$invoice_data{'total_items'}}, $total;
1908 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1911 foreach my $credit ( $self->_items_credits ) {
1913 $total->{'total_item'} = encode_entities($credit->{'description'});
1915 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1916 push @{$invoice_data{'total_items'}}, $total;
1920 foreach my $payment ( $self->_items_payments ) {
1922 $total->{'total_item'} = encode_entities($payment->{'description'});
1924 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1925 push @{$invoice_data{'total_items'}}, $total;
1930 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1931 $total->{'total_amount'} =
1932 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1933 push @{$invoice_data{'total_items'}}, $total;
1936 $html_template->fill_in( HASH => \%invoice_data);
1939 # quick subroutine for print_latex
1941 # There are ten characters that LaTeX treats as special characters, which
1942 # means that they do not simply typeset themselves:
1943 # # $ % & ~ _ ^ \ { }
1945 # TeX ignores blanks following an escaped character; if you want a blank (as
1946 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1950 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1951 $value =~ s/([<>])/\$$1\$/g;
1955 #utility methods for print_*
1957 sub balance_due_msg {
1959 my $msg = 'Balance Due';
1960 return $msg unless $conf->exists('invoice_default_terms');
1961 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1962 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1963 } elsif ( $conf->config('invoice_default_terms') ) {
1964 $msg .= ' - '. $conf->config('invoice_default_terms');
1971 my @display = scalar(@_)
1973 : qw( _items_previous _items_pkg );
1974 #: qw( _items_pkg );
1975 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1977 foreach my $display ( @display ) {
1978 push @b, $self->$display(@_);
1983 sub _items_previous {
1985 my $cust_main = $self->cust_main;
1986 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1988 foreach ( @pr_cust_bill ) {
1990 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1991 ' ('. time2str('%x',$_->_date). ')',
1992 #'pkgpart' => 'N/A',
1994 'amount' => sprintf("%.2f", $_->owed),
2000 # 'description' => 'Previous Balance',
2001 # #'pkgpart' => 'N/A',
2002 # 'pkgnum' => 'N/A',
2003 # 'amount' => sprintf("%10.2f", $pr_total ),
2004 # 'ext_description' => [ map {
2005 # "Invoice ". $_->invnum.
2006 # " (". time2str("%x",$_->_date). ") ".
2007 # sprintf("%10.2f", $_->owed)
2008 # } @pr_cust_bill ],
2015 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2016 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2021 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2022 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2025 sub _items_cust_bill_pkg {
2027 my $cust_bill_pkg = shift;
2030 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2032 my $desc = $cust_bill_pkg->desc;
2034 if ( $cust_bill_pkg->pkgnum > 0 ) {
2036 if ( $cust_bill_pkg->setup != 0 ) {
2037 my $description = $desc;
2038 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2039 my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2040 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2042 description => $description,
2043 #pkgpart => $part_pkg->pkgpart,
2044 pkgnum => $cust_bill_pkg->pkgnum,
2045 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2046 ext_description => \@d,
2050 if ( $cust_bill_pkg->recur != 0 ) {
2052 description => "$desc (" .
2053 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2054 time2str('%x', $cust_bill_pkg->edate). ')',
2055 #pkgpart => $part_pkg->pkgpart,
2056 pkgnum => $cust_bill_pkg->pkgnum,
2057 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2059 [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2060 $cust_bill_pkg->sdate),
2061 $cust_bill_pkg->details,
2066 } else { #pkgnum tax or one-shot line item (??)
2068 if ( $cust_bill_pkg->setup != 0 ) {
2070 'description' => $desc,
2071 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2074 if ( $cust_bill_pkg->recur != 0 ) {
2076 'description' => "$desc (".
2077 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2078 time2str("%x", $cust_bill_pkg->edate). ')',
2079 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2091 sub _items_credits {
2096 foreach ( $self->cust_credited ) {
2098 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2100 my $reason = $_->cust_credit->reason;
2101 #my $reason = substr($_->cust_credit->reason,0,32);
2102 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2103 $reason = " ($reason) " if $reason;
2105 #'description' => 'Credit ref\#'. $_->crednum.
2106 # " (". time2str("%x",$_->cust_credit->_date) .")".
2108 'description' => 'Credit applied '.
2109 time2str("%x",$_->cust_credit->_date). $reason,
2110 'amount' => sprintf("%.2f",$_->amount),
2113 #foreach ( @cr_cust_credit ) {
2115 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2116 # $money_char. sprintf("%10.2f",$_->credited)
2124 sub _items_payments {
2128 #get & print payments
2129 foreach ( $self->cust_bill_pay ) {
2131 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2134 'description' => "Payment received ".
2135 time2str("%x",$_->cust_pay->_date ),
2136 'amount' => sprintf("%.2f", $_->amount )
2154 sub process_reprint {
2155 process_re_X('print', @_);
2162 sub process_reemail {
2163 process_re_X('email', @_);
2171 process_re_X('fax', @_);
2174 use Storable qw(thaw);
2178 my( $method, $job ) = ( shift, shift );
2180 my $param = thaw(decode_base64(shift));
2181 warn Dumper($param) if $DEBUG;
2192 my($method, $job, %param ) = @_;
2193 # [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2195 #some false laziness w/search/cust_bill.html
2197 my $orderby = 'ORDER BY cust_bill._date';
2201 if ( $param{'begin'} =~ /^(\d+)$/ ) {
2202 push @where, "cust_bill._date >= $1";
2204 if ( $param{'end'} =~ /^(\d+)$/ ) {
2205 push @where, "cust_bill._date < $1";
2207 if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2208 push @where, "cust_main.agentnum = $1";
2212 "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2213 WHERE cust_bill_pay.invnum = cust_bill.invnum )
2214 - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2215 WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2217 push @where, "0 != $owed"
2220 push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2223 my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2225 my $addl_from = 'left join cust_main using ( custnum )';
2227 if ( $param{'newest_percust'} ) {
2228 $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2229 $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2230 #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2233 my @cust_bill = qsearch( 'cust_bill',
2235 "$distinct cust_bill.*",
2241 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2242 foreach my $cust_bill ( @cust_bill ) {
2243 $cust_bill->$method();
2245 if ( $job ) { #progressbar foo
2247 if ( time - $min_sec > $last ) {
2248 my $error = $job->update_statustext(
2249 int( 100 * $num / scalar(@cust_bill) )
2251 die $error if $error;
2266 print_text formatting (and some logic :/) is in source, but needs to be
2267 slurped in from a file. Also number of lines ($=).
2271 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2272 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base