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 use FS::cust_bill_pay;
25 use FS::part_bill_event;
27 @ISA = qw( FS::Record );
31 #ask FS::UID to run this stuff for us later
32 FS::UID->install_callback( sub {
34 $money_char = $conf->config('money_char') || '$';
39 FS::cust_bill - Object methods for cust_bill records
45 $record = new FS::cust_bill \%hash;
46 $record = new FS::cust_bill { 'column' => 'value' };
48 $error = $record->insert;
50 $error = $new_record->replace($old_record);
52 $error = $record->delete;
54 $error = $record->check;
56 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
58 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
60 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
62 @cust_pay_objects = $cust_bill->cust_pay;
64 $tax_amount = $record->tax;
66 @lines = $cust_bill->print_text;
67 @lines = $cust_bill->print_text $time;
71 An FS::cust_bill object represents an invoice; a declaration that a customer
72 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
73 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
74 following fields are currently supported:
78 =item invnum - primary key (assigned automatically for new invoices)
80 =item custnum - customer (see L<FS::cust_main>)
82 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
83 L<Time::Local> and L<Date::Parse> for conversion functions.
85 =item charged - amount of this invoice
87 =item printed - deprecated
89 =item closed - books closed flag, empty or `Y'
99 Creates a new invoice. To add the invoice to the database, see L<"insert">.
100 Invoices are normally created by calling the bill method of a customer object
101 (see L<FS::cust_main>).
105 sub table { 'cust_bill'; }
109 Adds this invoice to the database ("Posts" the invoice). If there is an error,
110 returns the error, otherwise returns false.
114 Currently unimplemented. I don't remove invoices because there would then be
115 no record you ever posted this invoice (which is bad, no?)
121 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
122 $self->SUPER::delete(@_);
125 =item replace OLD_RECORD
127 Replaces the OLD_RECORD with this one in the database. If there is an error,
128 returns the error, otherwise returns false.
130 Only printed may be changed. printed is normally updated by calling the
131 collect method of a customer object (see L<FS::cust_main>).
136 my( $new, $old ) = ( shift, shift );
137 return "Can't change custnum!" unless $old->custnum == $new->custnum;
138 #return "Can't change _date!" unless $old->_date eq $new->_date;
139 return "Can't change _date!" unless $old->_date == $new->_date;
140 return "Can't change charged!" unless $old->charged == $new->charged;
142 $new->SUPER::replace($old);
147 Checks all fields to make sure this is a valid invoice. If there is an error,
148 returns the error, otherwise returns false. Called by the insert and replace
157 $self->ut_numbern('invnum')
158 || $self->ut_number('custnum')
159 || $self->ut_numbern('_date')
160 || $self->ut_money('charged')
161 || $self->ut_numbern('printed')
162 || $self->ut_enum('closed', [ '', 'Y' ])
164 return $error if $error;
166 return "Unknown customer"
167 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
169 $self->_date(time) unless $self->_date;
171 $self->printed(0) if $self->printed eq '';
178 Returns a list consisting of the total previous balance for this customer,
179 followed by the previous outstanding invoices (as FS::cust_bill objects also).
186 my @cust_bill = sort { $a->_date <=> $b->_date }
187 grep { $_->owed != 0 && $_->_date < $self->_date }
188 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
190 foreach ( @cust_bill ) { $total += $_->owed; }
196 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
202 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
205 =item cust_bill_event
207 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
212 sub cust_bill_event {
214 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
220 Returns the customer (see L<FS::cust_main>) for this invoice.
226 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
231 Depreciated. See the cust_credited method.
233 #Returns a list consisting of the total previous credited (see
234 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
235 #outstanding credits (FS::cust_credit objects).
241 croak "FS::cust_bill->cust_credit depreciated; see ".
242 "FS::cust_bill->cust_credit_bill";
245 #my @cust_credit = sort { $a->_date <=> $b->_date }
246 # grep { $_->credited != 0 && $_->_date < $self->_date }
247 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
249 #foreach (@cust_credit) { $total += $_->credited; }
250 #$total, @cust_credit;
255 Depreciated. See the cust_bill_pay method.
257 #Returns all payments (see L<FS::cust_pay>) for this invoice.
263 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
265 #sort { $a->_date <=> $b->_date }
266 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
272 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
278 sort { $a->_date <=> $b->_date }
279 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
284 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
290 sort { $a->_date <=> $b->_date }
291 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
297 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
304 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
306 foreach (@taxlines) { $total += $_->setup; }
312 Returns the amount owed (still outstanding) on this invoice, which is charged
313 minus all payment applications (see L<FS::cust_bill_pay>) and credit
314 applications (see L<FS::cust_credit_bill>).
320 my $balance = $self->charged;
321 $balance -= $_->amount foreach ( $self->cust_bill_pay );
322 $balance -= $_->amount foreach ( $self->cust_credited );
323 $balance = sprintf( "%.2f", $balance);
324 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
329 =item generate_email PARAMHASH
331 PARAMHASH can contain the following:
335 =item from => sender address, required
337 =item tempate => alternate template name, optional
339 =item print_text => text attachment arrayref, optional
341 =item subject => email subject, optional
345 Returns an argument list to be passed to L<FS::Misc::send_email>.
356 my $me = '[FS::cust_bill::generate_email]';
359 'from' => $args{'from'},
360 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
363 if (ref($args{'to'} eq 'ARRAY')) {
364 $return{'to'} = $args{'to'};
366 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
367 $self->cust_main->invoicing_list
371 if ( $conf->exists('invoice_html') ) {
373 warn "$me creating HTML/text multipart message"
376 $return{'nobody'} = 1;
378 my $alternative = build MIME::Entity
379 'Type' => 'multipart/alternative',
380 'Encoding' => '7bit',
381 'Disposition' => 'inline'
385 if ( $conf->exists('invoice_email_pdf')
386 and scalar($conf->config('invoice_email_pdf_note')) ) {
388 warn "$me using 'invoice_email_pdf_note' in multipart message"
390 $data = [ map { $_ . "\n" }
391 $conf->config('invoice_email_pdf_note')
396 warn "$me not using 'invoice_email_pdf_note' in multipart message"
398 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
399 $data = $args{'print_text'};
401 $data = [ $self->print_text('', $args{'template'}) ];
406 $alternative->attach(
407 'Type' => 'text/plain',
408 #'Encoding' => 'quoted-printable',
409 'Encoding' => '7bit',
411 'Disposition' => 'inline',
414 $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
415 my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
417 my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
419 if ( [ -e "$path/logo_". $args{'_template'}. ".png" ] ) {
420 $file = "$path/logo_". $args{'_template'}. ".png";
422 $file = "$path/logo.png";
425 my $image = build MIME::Entity
426 'Type' => 'image/png',
427 'Encoding' => 'base64',
429 'Filename' => 'logo.png',
430 'Content-ID' => "<$content_id>",
433 $alternative->attach(
434 'Type' => 'text/html',
435 'Encoding' => 'quoted-printable',
436 'Data' => [ '<html>',
439 ' '. encode_entities($return{'subject'}),
442 ' <body bgcolor="#e8e8e8">',
443 $self->print_html('', $args{'template'}, $content_id),
447 'Disposition' => 'inline',
448 #'Filename' => 'invoice.pdf',
451 if ( $conf->exists('invoice_email_pdf') ) {
456 # multipart/alternative
462 my $related = build MIME::Entity 'Type' => 'multipart/related',
463 'Encoding' => '7bit';
465 #false laziness w/Misc::send_email
466 $related->head->replace('Content-type',
468 '; boundary="'. $related->head->multipart_boundary. '"'.
469 '; type=multipart/alternative'
472 $related->add_part($alternative);
474 $related->add_part($image);
476 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
478 $return{'mimeparts'} = [ $related, $pdf ];
482 #no other attachment:
484 # multipart/alternative
489 $return{'content-type'} = 'multipart/related';
490 $return{'mimeparts'} = [ $alternative, $image ];
491 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
492 #$return{'disposition'} = 'inline';
498 if ( $conf->exists('invoice_email_pdf') ) {
499 warn "$me creating PDF attachment"
502 #mime parts arguments a la MIME::Entity->build().
503 $return{'mimeparts'} = [
504 { $self->mimebuild_pdf('', $args{'template'}) }
508 if ( $conf->exists('invoice_email_pdf')
509 and scalar($conf->config('invoice_email_pdf_note')) ) {
511 warn "$me using 'invoice_email_pdf_note'"
513 $return{'body'} = [ map { $_ . "\n" }
514 $conf->config('invoice_email_pdf_note')
519 warn "$me not using 'invoice_email_pdf_note'"
521 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
522 $return{'body'} = $args{'print_text'};
524 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
537 Returns a list suitable for passing to MIME::Entity->build(), representing
538 this invoice as PDF attachment.
545 'Type' => 'application/pdf',
546 'Encoding' => 'base64',
547 'Data' => [ $self->print_pdf(@_) ],
548 'Disposition' => 'attachment',
549 'Filename' => 'invoice.pdf',
553 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
555 Sends this invoice to the destinations configured for this customer: send
556 emails or print. See L<FS::cust_main_invoice>.
558 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
560 AGENTNUM, if specified, means that this invoice will only be sent for customers
561 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
562 single agent) or an arrayref of agentnums.
564 INVOICE_FROM, if specified, overrides the default email invoice From: address.
570 my $template = scalar(@_) ? shift : '';
571 if ( scalar(@_) && $_[0] ) {
572 my $agentnums = ref($_[0]) ? shift : [ shift ];
573 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
579 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
581 my @invoicing_list = $self->cust_main->invoicing_list;
583 $self->email($template, $invoice_from)
584 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
586 $self->print($template)
587 if grep { $_ eq 'POST' } @invoicing_list; #postal
589 $self->fax($template)
590 if grep { $_ eq 'FAX' } @invoicing_list; #fax
596 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
600 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
602 INVOICE_FROM, if specified, overrides the default email invoice From: address.
608 my $template = scalar(@_) ? shift : '';
612 : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
614 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
615 $self->cust_main->invoicing_list;
617 #better to notify this person than silence
618 @invoicing_list = ($invoice_from) unless @invoicing_list;
620 my $error = send_email(
621 $self->generate_email(
622 'from' => $invoice_from,
623 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
624 'template' => $template,
627 die "can't email invoice: $error\n" if $error;
628 #die "$error\n" if $error;
632 =item lpr_data [ TEMPLATENAME ]
634 Returns the postscript or plaintext for this invoice.
636 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
641 my( $self, $template) = @_;
642 $conf->exists('invoice_latex')
643 ? [ $self->print_ps('', $template) ]
644 : [ $self->print_text('', $template) ];
647 =item print [ TEMPLATENAME ]
651 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
657 my $template = scalar(@_) ? shift : '';
659 my $lpr = $conf->config('lpr');
661 or die "Can't open pipe to $lpr: $!\n";
662 print LPR @{ $self->lpr_data($template) };
664 or die $! ? "Error closing $lpr: $!\n"
665 : "Exit status $? from $lpr\n";
668 =item fax [ TEMPLATENAME ]
672 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
678 my $template = scalar(@_) ? shift : '';
680 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
681 unless $conf->exists('invoice_latex');
683 my $dialstring = $self->cust_main->getfield('fax');
686 my $error = send_fax( 'docdata' => $self->lpr_data($template),
687 'dialstring' => $dialstring,
689 die $error if $error;
693 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
695 Like B<send>, but only sends the invoice if it is the newest open invoice for
705 grep { $_->owed > 0 }
706 qsearch('cust_bill', {
707 'custnum' => $self->custnum,
708 #'_date' => { op=>'>', value=>$self->_date },
709 'invnum' => { op=>'>', value=>$self->invnum },
716 =item send_csv OPTIONS
718 Sends invoice as a CSV data-file to a remote host with the specified protocol.
722 protocol - currently only "ftp"
728 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
729 and YYMMDDHHMMSS is a timestamp.
731 The fields of the CSV file is as follows:
733 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
737 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
739 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
740 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
741 fields are filled in.
743 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
744 first two fields (B<record_type> and B<invnum>) and the last five fields
745 (B<pkg> through B<edate>) are filled in.
747 =item invnum - invoice number
749 =item custnum - customer number
751 =item _date - invoice date
753 =item charged - total invoice amount
755 =item first - customer first name
757 =item last - customer first name
759 =item company - company name
761 =item address1 - address line 1
763 =item address2 - address line 1
773 =item pkg - line item description
775 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
777 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
779 =item sdate - start date for recurring fee
781 =item edate - end date for recurring fee
788 my($self, %opt) = @_;
790 #part one: create file
792 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
793 mkdir $spooldir, 0700 unless -d $spooldir;
795 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
797 open(CSV, ">$file") or die "can't open $file: $!";
799 eval "use Text::CSV_XS";
802 my $csv = Text::CSV_XS->new({'always_quote'=>1});
804 my $cust_main = $self->cust_main;
810 time2str("%x", $self->_date),
811 sprintf("%.2f", $self->charged),
812 ( map { $cust_main->getfield($_) }
813 qw( first last company address1 address2 city state zip country ) ),
815 ) or die "can't create csv";
816 print CSV $csv->string. "\n";
818 #new charges (false laziness w/print_text)
819 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
821 my($pkg, $setup, $recur, $sdate, $edate);
822 if ( $cust_bill_pkg->pkgnum ) {
824 ($pkg, $setup, $recur, $sdate, $edate) = (
825 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
826 ( $cust_bill_pkg->setup != 0
827 ? sprintf("%.2f", $cust_bill_pkg->setup )
829 ( $cust_bill_pkg->recur != 0
830 ? sprintf("%.2f", $cust_bill_pkg->recur )
832 time2str("%x", $cust_bill_pkg->sdate),
833 time2str("%x", $cust_bill_pkg->edate),
837 next unless $cust_bill_pkg->setup != 0;
838 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
839 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
841 ($pkg, $setup, $recur, $sdate, $edate) =
842 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
848 ( map { '' } (1..11) ),
849 ($pkg, $setup, $recur, $sdate, $edate)
850 ) or die "can't create csv";
851 print CSV $csv->string. "\n";
855 close CSV or die "can't close CSV: $!";
860 if ( $opt{protocol} eq 'ftp' ) {
861 eval "use Net::FTP;";
863 $net = Net::FTP->new($opt{server}) or die @$;
865 die "unknown protocol: $opt{protocol}";
868 $net->login( $opt{username}, $opt{password} )
869 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
871 $net->binary or die "can't set binary mode";
873 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
875 $net->put($file) or die "can't put $file: $!";
885 Pays this invoice with a compliemntary payment. If there is an error,
886 returns the error, otherwise returns false.
892 my $cust_pay = new FS::cust_pay ( {
893 'invnum' => $self->invnum,
894 'paid' => $self->owed,
897 'payinfo' => $self->cust_main->payinfo,
905 Attempts to pay this invoice with a credit card payment via a
906 Business::OnlinePayment realtime gateway. See
907 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
908 for supported processors.
914 $self->realtime_bop( 'CC', @_ );
919 Attempts to pay this invoice with an electronic check (ACH) 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( 'ECHECK', @_ );
933 Attempts to pay this invoice with phone bill (LEC) 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( 'LEC', @_ );
946 my( $self, $method ) = @_;
948 my $cust_main = $self->cust_main;
949 my $balance = $cust_main->balance;
950 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
951 $amount = sprintf("%.2f", $amount);
952 return "not run (balance $balance)" unless $amount > 0;
954 my $description = 'Internet Services';
955 if ( $conf->exists('business-onlinepayment-description') ) {
956 my $dtempl = $conf->config('business-onlinepayment-description');
958 my $agent_obj = $cust_main->agent
959 or die "can't retreive agent for $cust_main (agentnum ".
960 $cust_main->agentnum. ")";
961 my $agent = $agent_obj->agent;
962 my $pkgs = join(', ',
963 map { $_->cust_pkg->part_pkg->pkg }
964 grep { $_->pkgnum } $self->cust_bill_pkg
966 $description = eval qq("$dtempl");
969 $cust_main->realtime_bop($method, $amount,
970 'description' => $description,
971 'invnum' => $self->invnum,
978 Adds a payment for this invoice to the pending credit card batch (see
979 L<FS::cust_pay_batch>).
985 my $cust_main = $self->cust_main;
987 my $cust_pay_batch = new FS::cust_pay_batch ( {
988 'invnum' => $self->getfield('invnum'),
989 'custnum' => $cust_main->getfield('custnum'),
990 'last' => $cust_main->getfield('last'),
991 'first' => $cust_main->getfield('first'),
992 'address1' => $cust_main->getfield('address1'),
993 'address2' => $cust_main->getfield('address2'),
994 'city' => $cust_main->getfield('city'),
995 'state' => $cust_main->getfield('state'),
996 'zip' => $cust_main->getfield('zip'),
997 'country' => $cust_main->getfield('country'),
998 'cardnum' => $cust_main->payinfo,
999 'exp' => $cust_main->getfield('paydate'),
1000 'payname' => $cust_main->getfield('payname'),
1001 'amount' => $self->owed,
1003 my $error = $cust_pay_batch->insert;
1004 die $error if $error;
1009 sub _agent_template {
1011 $self->_agent_plandata('agent_templatename');
1014 sub _agent_invoice_from {
1016 $self->_agent_plandata('agent_invoice_from');
1019 sub _agent_plandata {
1020 my( $self, $option ) = @_;
1022 my $part_bill_event = qsearchs( 'part_bill_event',
1024 'payby' => $self->cust_main->payby,
1025 'plan' => 'send_agent',
1026 'plandata' => { 'op' => '~',
1027 'value' => "(^|\n)agentnum ".
1029 $self->cust_main->agentnum.
1035 'ORDER BY seconds LIMIT 1'
1038 return '' unless $part_bill_event;
1040 if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1043 warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1044 " plandata for $option";
1050 =item print_text [ TIME [ , TEMPLATE ] ]
1052 Returns an text invoice, as a list of lines.
1054 TIME an optional value used to control the printing of overdue messages. The
1055 default is now. It isn't the date of the invoice; that's the `_date' field.
1056 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1057 L<Time::Local> and L<Date::Parse> for conversion functions.
1061 #still some false laziness w/print_text
1064 my( $self, $today, $template ) = @_;
1067 # my $invnum = $self->invnum;
1068 my $cust_main = $self->cust_main;
1069 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1070 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1072 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1073 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1074 #my $balance_due = $self->owed + $pr_total - $cr_total;
1075 my $balance_due = $self->owed + $pr_total;
1078 #my($description,$amount);
1082 foreach ( @pr_cust_bill ) {
1084 "Previous Balance, Invoice #". $_->invnum.
1085 " (". time2str("%x",$_->_date). ")",
1086 $money_char. sprintf("%10.2f",$_->owed)
1089 if (@pr_cust_bill) {
1090 push @buf,['','-----------'];
1091 push @buf,[ 'Total Previous Balance',
1092 $money_char. sprintf("%10.2f",$pr_total ) ];
1097 foreach my $cust_bill_pkg (
1098 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1099 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1102 if ( $cust_bill_pkg->pkgnum > 0 ) {
1104 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1105 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1106 my $pkg = $part_pkg->pkg;
1108 if ( $cust_bill_pkg->setup != 0 ) {
1109 my $description = $pkg;
1110 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1111 push @buf, [ $description,
1112 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1114 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1115 $cust_pkg->h_labels($self->_date);
1118 if ( $cust_bill_pkg->recur != 0 ) {
1120 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1121 time2str("%x", $cust_bill_pkg->edate) . ")",
1122 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1125 map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
1126 $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1129 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
1131 } else { #pkgnum tax or one-shot line item
1132 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1133 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1135 if ( $cust_bill_pkg->setup != 0 ) {
1136 push @buf, [ $itemdesc,
1137 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1139 if ( $cust_bill_pkg->recur != 0 ) {
1140 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1141 . time2str("%x", $cust_bill_pkg->edate). ")",
1142 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1148 push @buf,['','-----------'];
1149 push @buf,['Total New Charges',
1150 $money_char. sprintf("%10.2f",$self->charged) ];
1153 push @buf,['','-----------'];
1154 push @buf,['Total Charges',
1155 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1159 foreach ( $self->cust_credited ) {
1161 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1163 my $reason = substr($_->cust_credit->reason,0,32);
1164 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1165 $reason = " ($reason) " if $reason;
1167 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1169 $money_char. sprintf("%10.2f",$_->amount)
1172 #foreach ( @cr_cust_credit ) {
1174 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1175 # $money_char. sprintf("%10.2f",$_->credited)
1179 #get & print payments
1180 foreach ( $self->cust_bill_pay ) {
1182 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1185 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1186 $money_char. sprintf("%10.2f",$_->amount )
1191 my $balance_due_msg = $self->balance_due_msg;
1193 push @buf,['','-----------'];
1194 push @buf,[$balance_due_msg, $money_char.
1195 sprintf("%10.2f", $balance_due ) ];
1197 #create the template
1198 $template ||= $self->_agent_template;
1199 my $templatefile = 'invoice_template';
1200 $templatefile .= "_$template" if length($template);
1201 my @invoice_template = $conf->config($templatefile)
1202 or die "cannot load config file $templatefile";
1205 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1206 /invoice_lines\((\d*)\)/;
1207 $invoice_lines += $1 || scalar(@buf);
1210 die "no invoice_lines() functions in template?" unless $wasfunc;
1211 my $invoice_template = new Text::Template (
1213 SOURCE => [ map "$_\n", @invoice_template ],
1214 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1215 $invoice_template->compile()
1216 or die "can't compile template: $Text::Template::ERROR";
1218 #setup template variables
1219 package FS::cust_bill::_template; #!
1220 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1222 $invnum = $self->invnum;
1223 $date = $self->_date;
1225 $agent = $self->cust_main->agent->agent;
1227 if ( $FS::cust_bill::invoice_lines ) {
1229 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1231 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1236 #format address (variable for the template)
1238 @address = ( '', '', '', '', '', '' );
1239 package FS::cust_bill; #!
1240 $FS::cust_bill::_template::address[$l++] =
1241 $cust_main->payname.
1242 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1243 ? " (P.O. #". $cust_main->payinfo. ")"
1247 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1248 if $cust_main->company;
1249 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1250 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1251 if $cust_main->address2;
1252 $FS::cust_bill::_template::address[$l++] =
1253 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1255 my $countrydefault = $conf->config('countrydefault') || 'US';
1256 $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1257 unless $cust_main->country eq $countrydefault;
1259 # #overdue? (variable for the template)
1260 # $FS::cust_bill::_template::overdue = (
1262 # && $today > $self->_date
1263 ## && $self->printed > 1
1264 # && $self->printed > 0
1267 #and subroutine for the template
1268 sub FS::cust_bill::_template::invoice_lines {
1269 my $lines = shift || scalar(@buf);
1271 scalar(@buf) ? shift @buf : [ '', '' ];
1277 $FS::cust_bill::_template::page = 1;
1281 push @collect, split("\n",
1282 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1284 $FS::cust_bill::_template::page++;
1287 map "$_\n", @collect;
1291 =item print_latex [ TIME [ , TEMPLATE ] ]
1293 Internal method - returns a filename of a filled-in LaTeX template for this
1294 invoice (Note: add ".tex" to get the actual filename).
1296 See print_ps and print_pdf for methods that return PostScript and PDF output.
1298 TIME an optional value used to control the printing of overdue messages. The
1299 default is now. It isn't the date of the invoice; that's the `_date' field.
1300 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1301 L<Time::Local> and L<Date::Parse> for conversion functions.
1305 #still some false laziness w/print_text
1308 my( $self, $today, $template ) = @_;
1310 warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1313 my $cust_main = $self->cust_main;
1314 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1315 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1317 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1318 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1319 #my $balance_due = $self->owed + $pr_total - $cr_total;
1320 my $balance_due = $self->owed + $pr_total;
1322 #create the template
1323 $template ||= $self->_agent_template;
1324 my $templatefile = 'invoice_latex';
1325 my $suffix = length($template) ? "_$template" : '';
1326 $templatefile .= $suffix;
1327 my @invoice_template = map "$_\n", $conf->config($templatefile)
1328 or die "cannot load config file $templatefile";
1330 my($format, $text_template);
1331 if ( grep { /^%%Detail/ } @invoice_template ) {
1332 #change this to a die when the old code is removed
1333 warn "old-style invoice template $templatefile; ".
1334 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1337 $format = 'Text::Template';
1338 $text_template = new Text::Template(
1340 SOURCE => \@invoice_template,
1341 DELIMITERS => [ '[@--', '--@]' ],
1344 $text_template->compile()
1345 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1349 if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1350 $returnaddress = join("\n",
1351 $conf->config_orbase('invoice_latexreturnaddress', $template)
1354 $returnaddress = '~';
1357 my %invoice_data = (
1358 'invnum' => $self->invnum,
1359 'date' => time2str('%b %o, %Y', $self->_date),
1360 'today' => time2str('%b %o, %Y', $today),
1361 'agent' => _latex_escape($cust_main->agent->agent),
1362 'payname' => _latex_escape($cust_main->payname),
1363 'company' => _latex_escape($cust_main->company),
1364 'address1' => _latex_escape($cust_main->address1),
1365 'address2' => _latex_escape($cust_main->address2),
1366 'city' => _latex_escape($cust_main->city),
1367 'state' => _latex_escape($cust_main->state),
1368 'zip' => _latex_escape($cust_main->zip),
1369 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1370 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1371 'returnaddress' => $returnaddress,
1373 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1374 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1375 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1378 my $countrydefault = $conf->config('countrydefault') || 'US';
1379 if ( $cust_main->country eq $countrydefault ) {
1380 $invoice_data{'country'} = '';
1382 $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1385 $invoice_data{'notes'} =
1387 # #do variable substitutions in notes
1388 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1389 $conf->config_orbase('invoice_latexnotes', $template)
1391 warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1394 $invoice_data{'footer'} =~ s/\n+$//;
1395 $invoice_data{'smallfooter'} =~ s/\n+$//;
1396 $invoice_data{'notes'} =~ s/\n+$//;
1398 $invoice_data{'po_line'} =
1399 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1400 ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1404 if ( $format eq 'old' ) {
1407 my @total_item = ();
1408 while ( @invoice_template ) {
1409 my $line = shift @invoice_template;
1411 if ( $line =~ /^%%Detail\s*$/ ) {
1413 while ( ( my $line_item_line = shift @invoice_template )
1414 !~ /^%%EndDetail\s*$/ ) {
1415 push @line_item, $line_item_line;
1417 foreach my $line_item ( $self->_items ) {
1418 #foreach my $line_item ( $self->_items_pkg ) {
1419 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1420 $invoice_data{'description'} =
1421 _latex_escape($line_item->{'description'});
1422 if ( exists $line_item->{'ext_description'} ) {
1423 $invoice_data{'description'} .=
1424 "\\tabularnewline\n~~".
1425 join( "\\tabularnewline\n~~",
1426 map _latex_escape($_), @{$line_item->{'ext_description'}}
1429 $invoice_data{'amount'} = $line_item->{'amount'};
1430 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1432 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1435 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1437 while ( ( my $total_item_line = shift @invoice_template )
1438 !~ /^%%EndTotalDetails\s*$/ ) {
1439 push @total_item, $total_item_line;
1442 my @total_fill = ();
1445 foreach my $tax ( $self->_items_tax ) {
1446 $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1447 $taxtotal += $tax->{'amount'};
1448 $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1450 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1455 $invoice_data{'total_item'} = 'Sub-total';
1456 $invoice_data{'total_amount'} =
1457 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1458 unshift @total_fill,
1459 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1463 $invoice_data{'total_item'} = '\textbf{Total}';
1464 $invoice_data{'total_amount'} =
1465 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1467 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1470 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1473 foreach my $credit ( $self->_items_credits ) {
1474 $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1476 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1478 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1483 foreach my $payment ( $self->_items_payments ) {
1484 $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1486 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1488 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1492 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1493 $invoice_data{'total_amount'} =
1494 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1496 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1499 push @filled_in, @total_fill;
1502 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1503 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1504 push @filled_in, $line;
1515 } elsif ( $format eq 'Text::Template' ) {
1517 my @detail_items = ();
1518 my @total_items = ();
1520 $invoice_data{'detail_items'} = \@detail_items;
1521 $invoice_data{'total_items'} = \@total_items;
1523 foreach my $line_item ( $self->_items ) {
1525 ext_description => [],
1527 $detail->{'ref'} = $line_item->{'pkgnum'};
1528 $detail->{'quantity'} = 1;
1529 $detail->{'description'} = _latex_escape($line_item->{'description'});
1530 if ( exists $line_item->{'ext_description'} ) {
1531 @{$detail->{'ext_description'}} = map {
1533 } @{$line_item->{'ext_description'}};
1535 $detail->{'amount'} = $line_item->{'amount'};
1536 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1538 push @detail_items, $detail;
1543 foreach my $tax ( $self->_items_tax ) {
1545 $total->{'total_item'} = _latex_escape($tax->{'description'});
1546 $taxtotal += $tax->{'amount'};
1547 $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1548 push @total_items, $total;
1553 $total->{'total_item'} = 'Sub-total';
1554 $total->{'total_amount'} =
1555 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1556 unshift @total_items, $total;
1561 $total->{'total_item'} = '\textbf{Total}';
1562 $total->{'total_amount'} =
1563 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1564 push @total_items, $total;
1567 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1570 foreach my $credit ( $self->_items_credits ) {
1572 $total->{'total_item'} = _latex_escape($credit->{'description'});
1574 $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1575 push @total_items, $total;
1579 foreach my $payment ( $self->_items_payments ) {
1581 $total->{'total_item'} = _latex_escape($payment->{'description'});
1583 $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1584 push @total_items, $total;
1589 $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1590 $total->{'total_amount'} =
1591 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1592 push @total_items, $total;
1596 die "guru meditation #54";
1599 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1600 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1604 ) or die "can't open temp file: $!\n";
1605 if ( $format eq 'old' ) {
1606 print $fh join('', @filled_in );
1607 } elsif ( $format eq 'Text::Template' ) {
1608 $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1610 die "guru meditation #32";
1614 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1619 =item print_ps [ TIME [ , TEMPLATE ] ]
1621 Returns an postscript invoice, as a scalar.
1623 TIME an optional value used to control the printing of overdue messages. The
1624 default is now. It isn't the date of the invoice; that's the `_date' field.
1625 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1626 L<Time::Local> and L<Date::Parse> for conversion functions.
1633 my $file = $self->print_latex(@_);
1635 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1638 my $sfile = shell_quote $file;
1640 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1641 or die "pslatex $file.tex failed; see $file.log for details?\n";
1642 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1643 or die "pslatex $file.tex failed; see $file.log for details?\n";
1645 system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1646 or die "dvips failed";
1648 open(POSTSCRIPT, "<$file.ps")
1649 or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1651 unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1654 while (<POSTSCRIPT>) {
1664 =item print_pdf [ TIME [ , TEMPLATE ] ]
1666 Returns an PDF invoice, as a scalar.
1668 TIME an optional value used to control the printing of overdue messages. The
1669 default is now. It isn't the date of the invoice; that's the `_date' field.
1670 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1671 L<Time::Local> and L<Date::Parse> for conversion functions.
1678 my $file = $self->print_latex(@_);
1680 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1683 #system('pdflatex', "$file.tex");
1684 #system('pdflatex', "$file.tex");
1685 #! LaTeX Error: Unknown graphics extension: .eps.
1687 my $sfile = shell_quote $file;
1689 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1690 or die "pslatex $file.tex failed; see $file.log for details?\n";
1691 system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1692 or die "pslatex $file.tex failed; see $file.log for details?\n";
1694 #system('dvipdf', "$file.dvi", "$file.pdf" );
1696 "dvips -q -t letter -f $sfile.dvi ".
1697 "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1700 or die "dvips | gs failed: $!";
1702 open(PDF, "<$file.pdf")
1703 or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1705 unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1718 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1720 Returns an HTML invoice, as a scalar.
1722 TIME an optional value used to control the printing of overdue messages. The
1723 default is now. It isn't the date of the invoice; that's the `_date' field.
1724 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1725 L<Time::Local> and L<Date::Parse> for conversion functions.
1727 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1728 when emailing the invoice as part of a multipart/related MIME email.
1733 my( $self, $today, $template, $cid ) = @_;
1736 my $cust_main = $self->cust_main;
1737 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1738 unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1740 $template ||= $self->_agent_template;
1741 my $templatefile = 'invoice_html';
1742 my $suffix = length($template) ? "_$template" : '';
1743 $templatefile .= $suffix;
1744 my @html_template = map "$_\n", $conf->config($templatefile)
1745 or die "cannot load config file $templatefile";
1747 my $html_template = new Text::Template(
1749 SOURCE => \@html_template,
1750 DELIMITERS => [ '<%=', '%>' ],
1753 $html_template->compile()
1754 or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1756 my %invoice_data = (
1757 'invnum' => $self->invnum,
1758 'date' => time2str('%b %o, %Y', $self->_date),
1759 'today' => time2str('%b %o, %Y', $today),
1760 'agent' => encode_entities($cust_main->agent->agent),
1761 'payname' => encode_entities($cust_main->payname),
1762 'company' => encode_entities($cust_main->company),
1763 'address1' => encode_entities($cust_main->address1),
1764 'address2' => encode_entities($cust_main->address2),
1765 'city' => encode_entities($cust_main->city),
1766 'state' => encode_entities($cust_main->state),
1767 'zip' => encode_entities($cust_main->zip),
1768 'terms' => $conf->config('invoice_default_terms')
1769 || 'Payable upon receipt',
1771 'template' => $template,
1772 # 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1775 $invoice_data{'returnaddress'} =
1776 length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1777 ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1780 s/\\\\\*?\s*$/<BR>/;
1781 s/\\hyphenation\{[\w\s\-]+\}//;
1784 $conf->config_orbase('invoice_latexreturnaddress', $template)
1787 my $countrydefault = $conf->config('countrydefault') || 'US';
1788 if ( $cust_main->country eq $countrydefault ) {
1789 $invoice_data{'country'} = '';
1791 $invoice_data{'country'} =
1792 encode_entities(code2country($cust_main->country));
1795 $invoice_data{'notes'} =
1796 length($conf->config_orbase('invoice_htmlnotes', $template))
1797 ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1799 s/%%(.*)$/<!-- $1 -->/;
1800 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1801 s/\\begin\{enumerate\}/<ol>/;
1803 s/\\end\{enumerate\}/<\/ol>/;
1804 s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1807 $conf->config_orbase('invoice_latexnotes', $template)
1810 # #do variable substitutions in notes
1811 # $invoice_data{'notes'} =
1813 # map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1814 # $conf->config_orbase('invoice_latexnotes', $suffix)
1817 $invoice_data{'footer'} =
1818 length($conf->config_orbase('invoice_htmlfooter', $template))
1819 ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) )
1820 : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
1821 $conf->config_orbase('invoice_latexfooter', $template)
1824 $invoice_data{'po_line'} =
1825 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1826 ? encode_entities("Purchase Order #". $cust_main->payinfo)
1829 my $money_char = $conf->config('money_char') || '$';
1831 foreach my $line_item ( $self->_items ) {
1833 ext_description => [],
1835 $detail->{'ref'} = $line_item->{'pkgnum'};
1836 $detail->{'description'} = encode_entities($line_item->{'description'});
1837 if ( exists $line_item->{'ext_description'} ) {
1838 @{$detail->{'ext_description'}} = map {
1839 encode_entities($_);
1840 } @{$line_item->{'ext_description'}};
1842 $detail->{'amount'} = $money_char. $line_item->{'amount'};
1843 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1845 push @{$invoice_data{'detail_items'}}, $detail;
1850 foreach my $tax ( $self->_items_tax ) {
1852 $total->{'total_item'} = encode_entities($tax->{'description'});
1853 $taxtotal += $tax->{'amount'};
1854 $total->{'total_amount'} = $money_char. $tax->{'amount'};
1855 push @{$invoice_data{'total_items'}}, $total;
1860 $total->{'total_item'} = 'Sub-total';
1861 $total->{'total_amount'} =
1862 $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1863 unshift @{$invoice_data{'total_items'}}, $total;
1866 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1869 $total->{'total_item'} = '<b>Total</b>';
1870 $total->{'total_amount'} =
1871 "<b>$money_char". sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1872 push @{$invoice_data{'total_items'}}, $total;
1875 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1878 foreach my $credit ( $self->_items_credits ) {
1880 $total->{'total_item'} = encode_entities($credit->{'description'});
1882 $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1883 push @{$invoice_data{'total_items'}}, $total;
1887 foreach my $payment ( $self->_items_payments ) {
1889 $total->{'total_item'} = encode_entities($payment->{'description'});
1891 $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1892 push @{$invoice_data{'total_items'}}, $total;
1897 $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1898 $total->{'total_amount'} =
1899 "<b>$money_char". sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1900 push @{$invoice_data{'total_items'}}, $total;
1903 $html_template->fill_in( HASH => \%invoice_data);
1906 # quick subroutine for print_latex
1908 # There are ten characters that LaTeX treats as special characters, which
1909 # means that they do not simply typeset themselves:
1910 # # $ % & ~ _ ^ \ { }
1912 # TeX ignores blanks following an escaped character; if you want a blank (as
1913 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1917 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1918 $value =~ s/([<>])/\$$1\$/g;
1922 #utility methods for print_*
1924 sub balance_due_msg {
1926 my $msg = 'Balance Due';
1927 return $msg unless $conf->exists('invoice_default_terms');
1928 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1929 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1930 } elsif ( $conf->config('invoice_default_terms') ) {
1931 $msg .= ' - '. $conf->config('invoice_default_terms');
1938 my @display = scalar(@_)
1940 : qw( _items_previous _items_pkg );
1941 #: qw( _items_pkg );
1942 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1944 foreach my $display ( @display ) {
1945 push @b, $self->$display(@_);
1950 sub _items_previous {
1952 my $cust_main = $self->cust_main;
1953 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1955 foreach ( @pr_cust_bill ) {
1957 'description' => 'Previous Balance, Invoice #'. $_->invnum.
1958 ' ('. time2str('%x',$_->_date). ')',
1959 #'pkgpart' => 'N/A',
1961 'amount' => sprintf("%.2f", $_->owed),
1967 # 'description' => 'Previous Balance',
1968 # #'pkgpart' => 'N/A',
1969 # 'pkgnum' => 'N/A',
1970 # 'amount' => sprintf("%10.2f", $pr_total ),
1971 # 'ext_description' => [ map {
1972 # "Invoice ". $_->invnum.
1973 # " (". time2str("%x",$_->_date). ") ".
1974 # sprintf("%10.2f", $_->owed)
1975 # } @pr_cust_bill ],
1982 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1983 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1988 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1989 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1992 sub _items_cust_bill_pkg {
1994 my $cust_bill_pkg = shift;
1997 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1999 if ( $cust_bill_pkg->pkgnum > 0 ) {
2001 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
2002 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
2003 my $pkg = $part_pkg->pkg;
2005 if ( $cust_bill_pkg->setup != 0 ) {
2006 my $description = $pkg;
2007 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2008 my @d = $cust_pkg->h_labels_short($self->_date);
2009 push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2011 description => $description,
2012 #pkgpart => $part_pkg->pkgpart,
2013 pkgnum => $cust_pkg->pkgnum,
2014 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2015 ext_description => \@d,
2019 if ( $cust_bill_pkg->recur != 0 ) {
2021 description => "$pkg (" .
2022 time2str('%x', $cust_bill_pkg->sdate). ' - '.
2023 time2str('%x', $cust_bill_pkg->edate). ')',
2024 #pkgpart => $part_pkg->pkgpart,
2025 pkgnum => $cust_pkg->pkgnum,
2026 amount => sprintf("%.2f", $cust_bill_pkg->recur),
2027 ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2028 $cust_bill_pkg->sdate),
2029 $cust_bill_pkg->details,
2034 } else { #pkgnum tax or one-shot line item (??)
2036 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2037 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2039 if ( $cust_bill_pkg->setup != 0 ) {
2041 'description' => $itemdesc,
2042 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2045 if ( $cust_bill_pkg->recur != 0 ) {
2047 'description' => "$itemdesc (".
2048 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2049 time2str("%x", $cust_bill_pkg->edate). ')',
2050 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2062 sub _items_credits {
2067 foreach ( $self->cust_credited ) {
2069 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2071 my $reason = $_->cust_credit->reason;
2072 #my $reason = substr($_->cust_credit->reason,0,32);
2073 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2074 $reason = " ($reason) " if $reason;
2076 #'description' => 'Credit ref\#'. $_->crednum.
2077 # " (". time2str("%x",$_->cust_credit->_date) .")".
2079 'description' => 'Credit applied '.
2080 time2str("%x",$_->cust_credit->_date). $reason,
2081 'amount' => sprintf("%.2f",$_->amount),
2084 #foreach ( @cr_cust_credit ) {
2086 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2087 # $money_char. sprintf("%10.2f",$_->credited)
2095 sub _items_payments {
2099 #get & print payments
2100 foreach ( $self->cust_bill_pay ) {
2102 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2105 'description' => "Payment received ".
2106 time2str("%x",$_->cust_pay->_date ),
2107 'amount' => sprintf("%.2f", $_->amount )
2125 sub process_reprint {
2126 process_re_X('print', @_);
2133 sub process_reemail {
2134 process_re_X('email', @_);
2142 process_re_X('fax', @_);
2145 use Storable qw(thaw);
2149 my( $method, $job ) = ( shift, shift );
2151 my $param = thaw(decode_base64(shift));
2152 warn Dumper($param) if $DEBUG;
2156 $param->{'beginning'},
2165 my($method, $beginning, $ending, $failed, $job) = @_;
2167 my $where = " WHERE plan LIKE 'send%'".
2168 " AND cust_bill_event._date >= $beginning".
2169 " AND cust_bill_event._date <= $ending";
2170 $where .= " AND statustext != '' AND statustext IS NOT NULL"
2173 my $from = 'LEFT JOIN part_bill_event USING ( eventpart )';
2175 my @cust_bill_event = qsearch( 'cust_bill_event', {}, '', $where, '', $from );
2177 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2178 foreach my $cust_bill_event ( @cust_bill_event ) {
2180 $cust_bill_event->cust_bill->$method(
2181 $cust_bill_event->part_bill_event->templatename
2184 if ( $job ) { #progressbar foo
2186 if ( time - $min_sec > $last ) {
2187 my $error = $job->update_statustext(
2188 int( 100 * $num / scalar(@cust_bill_event) )
2190 die $error if $error;
2197 #this doesn't work, but it would be nice
2198 #if ( $job ) { #progressbar foo
2199 # my $error = $job->update_statustext(
2200 # scalar(@cust_bill_event). " invoices re-${method}ed"
2202 # die $error if $error;
2213 print_text formatting (and some logic :/) is in source, but needs to be
2214 slurped in from a file. Also number of lines ($=).
2218 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2219 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base