4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
20 use FS::cust_bill_pkg_display;
24 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
30 use FS::cust_bill_pay;
31 use FS::cust_bill_pay_batch;
32 use FS::part_bill_event;
35 @ISA = qw( FS::cust_main_Mixin FS::Record );
38 $me = '[FS::cust_bill]';
40 #ask FS::UID to run this stuff for us later
41 FS::UID->install_callback( sub {
43 $money_char = $conf->config('money_char') || '$';
48 FS::cust_bill - Object methods for cust_bill records
54 $record = new FS::cust_bill \%hash;
55 $record = new FS::cust_bill { 'column' => 'value' };
57 $error = $record->insert;
59 $error = $new_record->replace($old_record);
61 $error = $record->delete;
63 $error = $record->check;
65 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
67 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
69 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
71 @cust_pay_objects = $cust_bill->cust_pay;
73 $tax_amount = $record->tax;
75 @lines = $cust_bill->print_text;
76 @lines = $cust_bill->print_text $time;
80 An FS::cust_bill object represents an invoice; a declaration that a customer
81 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
82 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
83 following fields are currently supported:
87 =item invnum - primary key (assigned automatically for new invoices)
89 =item custnum - customer (see L<FS::cust_main>)
91 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
92 L<Time::Local> and L<Date::Parse> for conversion functions.
94 =item charged - amount of this invoice
96 =item printed - deprecated
98 =item closed - books closed flag, empty or `Y'
108 Creates a new invoice. To add the invoice to the database, see L<"insert">.
109 Invoices are normally created by calling the bill method of a customer object
110 (see L<FS::cust_main>).
114 sub table { 'cust_bill'; }
116 sub cust_linked { $_[0]->cust_main_custnum; }
117 sub cust_unlinked_msg {
119 "WARNING: can't find cust_main.custnum ". $self->custnum.
120 ' (cust_bill.invnum '. $self->invnum. ')';
125 Adds this invoice to the database ("Posts" the invoice). If there is an error,
126 returns the error, otherwise returns false.
130 This method now works but you probably shouldn't use it. Instead, apply a
131 credit against the invoice.
133 Using this method to delete invoices outright is really, really bad. There
134 would be no record you ever posted this invoice, and there are no check to
135 make sure charged = 0 or that there are no associated cust_bill_pkg records.
137 Really, don't use it.
143 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
144 $self->SUPER::delete(@_);
147 =item replace OLD_RECORD
149 Replaces the OLD_RECORD with this one in the database. If there is an error,
150 returns the error, otherwise returns false.
152 Only printed may be changed. printed is normally updated by calling the
153 collect method of a customer object (see L<FS::cust_main>).
157 #replace can be inherited from Record.pm
159 # replace_check is now the preferred way to #implement replace data checks
160 # (so $object->replace() works without an argument)
163 my( $new, $old ) = ( shift, shift );
164 return "Can't change custnum!" unless $old->custnum == $new->custnum;
165 #return "Can't change _date!" unless $old->_date eq $new->_date;
166 return "Can't change _date!" unless $old->_date == $new->_date;
167 return "Can't change charged!" unless $old->charged == $new->charged
168 || $old->charged == 0;
175 Checks all fields to make sure this is a valid invoice. If there is an error,
176 returns the error, otherwise returns false. Called by the insert and replace
185 $self->ut_numbern('invnum')
186 || $self->ut_number('custnum')
187 || $self->ut_numbern('_date')
188 || $self->ut_money('charged')
189 || $self->ut_numbern('printed')
190 || $self->ut_enum('closed', [ '', 'Y' ])
192 return $error if $error;
194 return "Unknown customer"
195 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
197 $self->_date(time) unless $self->_date;
199 $self->printed(0) if $self->printed eq '';
206 Returns a list consisting of the total previous balance for this customer,
207 followed by the previous outstanding invoices (as FS::cust_bill objects also).
214 my @cust_bill = sort { $a->_date <=> $b->_date }
215 grep { $_->owed != 0 && $_->_date < $self->_date }
216 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
218 foreach ( @cust_bill ) { $total += $_->owed; }
224 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
231 { 'table' => 'cust_bill_pkg',
232 'hashref' => { 'invnum' => $self->invnum },
233 'order_by' => 'ORDER BY billpkgnum',
240 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
247 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
249 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
252 =item open_cust_bill_pkg
254 Returns the open line items for this invoice.
256 Note that cust_bill_pkg with both setup and recur fees are returned as two
257 separate line items, each with only one fee.
261 # modeled after cust_main::open_cust_bill
262 sub open_cust_bill_pkg {
265 # grep { $_->owed > 0 } $self->cust_bill_pkg
267 my %other = ( 'recur' => 'setup',
268 'setup' => 'recur', );
270 foreach my $field ( qw( recur setup )) {
271 push @open, map { $_->set( $other{$field}, 0 ); $_; }
272 grep { $_->owed($field) > 0 }
273 $self->cust_bill_pkg;
279 =item cust_bill_event
281 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
285 sub cust_bill_event {
287 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
290 =item num_cust_bill_event
292 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
296 sub num_cust_bill_event {
299 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
300 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
301 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
302 $sth->fetchrow_arrayref->[0];
307 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
311 #false laziness w/cust_pkg.pm
315 'table' => 'cust_event',
316 'addl_from' => 'JOIN part_event USING ( eventpart )',
317 'hashref' => { 'tablenum' => $self->invnum },
318 'extra_sql' => " AND eventtable = 'cust_bill' ",
324 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
328 #false laziness w/cust_pkg.pm
332 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
333 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
334 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
335 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
336 $sth->fetchrow_arrayref->[0];
341 Returns the customer (see L<FS::cust_main>) for this invoice.
347 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
350 =item cust_suspend_if_balance_over AMOUNT
352 Suspends the customer associated with this invoice if the total amount owed on
353 this invoice and all older invoices is greater than the specified amount.
355 Returns a list: an empty list on success or a list of errors.
359 sub cust_suspend_if_balance_over {
360 my( $self, $amount ) = ( shift, shift );
361 my $cust_main = $self->cust_main;
362 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
365 $cust_main->suspend(@_);
371 Depreciated. See the cust_credited method.
373 #Returns a list consisting of the total previous credited (see
374 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
375 #outstanding credits (FS::cust_credit objects).
381 croak "FS::cust_bill->cust_credit depreciated; see ".
382 "FS::cust_bill->cust_credit_bill";
385 #my @cust_credit = sort { $a->_date <=> $b->_date }
386 # grep { $_->credited != 0 && $_->_date < $self->_date }
387 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
389 #foreach (@cust_credit) { $total += $_->credited; }
390 #$total, @cust_credit;
395 Depreciated. See the cust_bill_pay method.
397 #Returns all payments (see L<FS::cust_pay>) for this invoice.
403 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
405 #sort { $a->_date <=> $b->_date }
406 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
412 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
418 sort { $a->_date <=> $b->_date }
419 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
424 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
430 sort { $a->_date <=> $b->_date }
431 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
437 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
444 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
446 foreach (@taxlines) { $total += $_->setup; }
452 Returns the amount owed (still outstanding) on this invoice, which is charged
453 minus all payment applications (see L<FS::cust_bill_pay>) and credit
454 applications (see L<FS::cust_credit_bill>).
460 my $balance = $self->charged;
461 $balance -= $_->amount foreach ( $self->cust_bill_pay );
462 $balance -= $_->amount foreach ( $self->cust_credited );
463 $balance = sprintf( "%.2f", $balance);
464 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
468 =item apply_payments_and_credits
472 sub apply_payments_and_credits {
475 local $SIG{HUP} = 'IGNORE';
476 local $SIG{INT} = 'IGNORE';
477 local $SIG{QUIT} = 'IGNORE';
478 local $SIG{TERM} = 'IGNORE';
479 local $SIG{TSTP} = 'IGNORE';
480 local $SIG{PIPE} = 'IGNORE';
482 my $oldAutoCommit = $FS::UID::AutoCommit;
483 local $FS::UID::AutoCommit = 0;
486 $self->select_for_update; #mutex
488 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
489 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
491 while ( $self->owed > 0 and ( @payments || @credits ) ) {
494 if ( @payments && @credits ) {
496 #decide which goes first by weight of top (unapplied) line item
498 my @open_lineitems = $self->open_cust_bill_pkg;
501 max( map { $_->part_pkg->pay_weight || 0 }
506 my $max_credit_weight =
507 max( map { $_->part_pkg->credit_weight || 0 }
513 #if both are the same... payments first? it has to be something
514 if ( $max_pay_weight >= $max_credit_weight ) {
520 } elsif ( @payments ) {
522 } elsif ( @credits ) {
525 die "guru meditation #12 and 35";
528 if ( $app eq 'pay' ) {
530 my $payment = shift @payments;
532 $app = new FS::cust_bill_pay {
533 'paynum' => $payment->paynum,
534 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
537 } elsif ( $app eq 'credit' ) {
539 my $credit = shift @credits;
541 $app = new FS::cust_credit_bill {
542 'crednum' => $credit->crednum,
543 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
547 die "guru meditation #12 and 35";
550 $app->invnum( $self->invnum );
552 my $error = $app->insert;
554 $dbh->rollback if $oldAutoCommit;
555 return "Error inserting ". $app->table. " record: $error";
557 die $error if $error;
561 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
566 =item generate_email OPTION => VALUE ...
574 sender address, required
578 alternate template name, optional
582 text attachment arrayref, optional
586 email subject, optional
590 Returns an argument list to be passed to L<FS::Misc::send_email>.
601 my $me = '[FS::cust_bill::generate_email]';
604 'from' => $args{'from'},
605 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
608 if (ref($args{'to'}) eq 'ARRAY') {
609 $return{'to'} = $args{'to'};
611 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
612 $self->cust_main->invoicing_list
616 if ( $conf->exists('invoice_html') ) {
618 warn "$me creating HTML/text multipart message"
621 $return{'nobody'} = 1;
623 my $alternative = build MIME::Entity
624 'Type' => 'multipart/alternative',
625 'Encoding' => '7bit',
626 'Disposition' => 'inline'
630 if ( $conf->exists('invoice_email_pdf')
631 and scalar($conf->config('invoice_email_pdf_note')) ) {
633 warn "$me using 'invoice_email_pdf_note' in multipart message"
635 $data = [ map { $_ . "\n" }
636 $conf->config('invoice_email_pdf_note')
641 warn "$me not using 'invoice_email_pdf_note' in multipart message"
643 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
644 $data = $args{'print_text'};
646 $data = [ $self->print_text('', $args{'template'}) ];
651 $alternative->attach(
652 'Type' => 'text/plain',
653 #'Encoding' => 'quoted-printable',
654 'Encoding' => '7bit',
656 'Disposition' => 'inline',
659 $args{'from'} =~ /\@([\w\.\-]+)/;
660 my $from = $1 || 'example.com';
661 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
664 my $agentnum = $self->cust_main->agentnum;
665 if ( defined($args{'template'}) && length($args{'template'})
666 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
669 $logo = 'logo_'. $args{'template'}. '.png';
673 my $image_data = $conf->config_binary( $logo, $agentnum);
675 my $image = build MIME::Entity
676 'Type' => 'image/png',
677 'Encoding' => 'base64',
678 'Data' => $image_data,
679 'Filename' => 'logo.png',
680 'Content-ID' => "<$content_id>",
683 $alternative->attach(
684 'Type' => 'text/html',
685 'Encoding' => 'quoted-printable',
686 'Data' => [ '<html>',
689 ' '. encode_entities($return{'subject'}),
692 ' <body bgcolor="#e8e8e8">',
693 $self->print_html('', $args{'template'}, $content_id),
697 'Disposition' => 'inline',
698 #'Filename' => 'invoice.pdf',
701 if ( $conf->exists('invoice_email_pdf') ) {
706 # multipart/alternative
712 my $related = build MIME::Entity 'Type' => 'multipart/related',
713 'Encoding' => '7bit';
715 #false laziness w/Misc::send_email
716 $related->head->replace('Content-type',
718 '; boundary="'. $related->head->multipart_boundary. '"'.
719 '; type=multipart/alternative'
722 $related->add_part($alternative);
724 $related->add_part($image);
726 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
728 $return{'mimeparts'} = [ $related, $pdf ];
732 #no other attachment:
734 # multipart/alternative
739 $return{'content-type'} = 'multipart/related';
740 $return{'mimeparts'} = [ $alternative, $image ];
741 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
742 #$return{'disposition'} = 'inline';
748 if ( $conf->exists('invoice_email_pdf') ) {
749 warn "$me creating PDF attachment"
752 #mime parts arguments a la MIME::Entity->build().
753 $return{'mimeparts'} = [
754 { $self->mimebuild_pdf('', $args{'template'}) }
758 if ( $conf->exists('invoice_email_pdf')
759 and scalar($conf->config('invoice_email_pdf_note')) ) {
761 warn "$me using 'invoice_email_pdf_note'"
763 $return{'body'} = [ map { $_ . "\n" }
764 $conf->config('invoice_email_pdf_note')
769 warn "$me not using 'invoice_email_pdf_note'"
771 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
772 $return{'body'} = $args{'print_text'};
774 $return{'body'} = [ $self->print_text('', $args{'template'}) ];
787 Returns a list suitable for passing to MIME::Entity->build(), representing
788 this invoice as PDF attachment.
795 'Type' => 'application/pdf',
796 'Encoding' => 'base64',
797 'Data' => [ $self->print_pdf(@_) ],
798 'Disposition' => 'attachment',
799 'Filename' => 'invoice.pdf',
803 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
805 Sends this invoice to the destinations configured for this customer: sends
806 email, prints and/or faxes. See L<FS::cust_main_invoice>.
808 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
810 AGENTNUM, if specified, means that this invoice will only be sent for customers
811 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
812 single agent) or an arrayref of agentnums.
814 INVOICE_FROM, if specified, overrides the default email invoice From: address.
816 AMOUNT, if specified, only sends the invoice if the total amount owed on this
817 invoice and all older invoices is greater than the specified amount.
824 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
825 or die "invalid invoice number: " . $opt{invnum};
827 my @args = ( $opt{template}, $opt{agentnum} );
828 push @args, $opt{invoice_from}
829 if exists($opt{invoice_from}) && $opt{invoice_from};
831 my $error = $self->send( @args );
832 die $error if $error;
838 my $template = scalar(@_) ? shift : '';
839 if ( scalar(@_) && $_[0] ) {
840 my $agentnums = ref($_[0]) ? shift : [ shift ];
841 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
847 : ( $self->_agent_invoice_from || #XXX should go away
848 $conf->config('invoice_from', $self->cust_main->agentnum )
851 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
854 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
856 my @invoicing_list = $self->cust_main->invoicing_list;
858 #$self->email_invoice($template, $invoice_from)
859 $self->email($template, $invoice_from)
860 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
862 #$self->print_invoice($template)
863 $self->print($template)
864 if grep { $_ eq 'POST' } @invoicing_list; #postal
866 $self->fax_invoice($template)
867 if grep { $_ eq 'FAX' } @invoicing_list; #fax
873 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
877 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
879 INVOICE_FROM, if specified, overrides the default email invoice From: address.
883 sub queueable_email {
886 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
887 or die "invalid invoice number: " . $opt{invnum};
889 my @args = ( $opt{template} );
890 push @args, $opt{invoice_from}
891 if exists($opt{invoice_from}) && $opt{invoice_from};
893 my $error = $self->email( @args );
894 die $error if $error;
901 my $template = scalar(@_) ? shift : '';
905 : ( $self->_agent_invoice_from || #XXX should go away
906 $conf->config('invoice_from', $self->cust_main->agentnum )
910 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
911 $self->cust_main->invoicing_list;
913 #better to notify this person than silence
914 @invoicing_list = ($invoice_from) unless @invoicing_list;
916 my $subject = $self->email_subject($template);
918 my $error = send_email(
919 $self->generate_email(
920 'from' => $invoice_from,
921 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
922 'subject' => $subject,
923 'template' => $template,
926 die "can't email invoice: $error\n" if $error;
927 #die "$error\n" if $error;
934 #my $template = scalar(@_) ? shift : '';
937 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
940 my $cust_main = $self->cust_main;
941 my $name = $cust_main->name;
942 my $name_short = $cust_main->name_short;
943 my $invoice_number = $self->invnum;
944 my $invoice_date = $self->_date_pretty;
949 =item lpr_data [ TEMPLATENAME ]
951 Returns the postscript or plaintext for this invoice as an arrayref.
953 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
958 my( $self, $template) = @_;
959 $conf->exists('invoice_latex')
960 ? [ $self->print_ps('', $template) ]
961 : [ $self->print_text('', $template) ];
964 =item print [ TEMPLATENAME ]
968 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
975 my $template = scalar(@_) ? shift : '';
977 do_print $self->lpr_data($template);
980 =item fax_invoice [ TEMPLATENAME ]
984 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
990 my $template = scalar(@_) ? shift : '';
992 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
993 unless $conf->exists('invoice_latex');
995 my $dialstring = $self->cust_main->getfield('fax');
998 my $error = send_fax( 'docdata' => $self->lpr_data($template),
999 'dialstring' => $dialstring,
1001 die $error if $error;
1005 =item ftp_invoice [ TEMPLATENAME ]
1007 Sends this invoice data via FTP.
1009 TEMPLATENAME is unused?
1015 my $template = scalar(@_) ? shift : '';
1018 'protocol' => 'ftp',
1019 'server' => $conf->config('cust_bill-ftpserver'),
1020 'username' => $conf->config('cust_bill-ftpusername'),
1021 'password' => $conf->config('cust_bill-ftppassword'),
1022 'dir' => $conf->config('cust_bill-ftpdir'),
1023 'format' => $conf->config('cust_bill-ftpformat'),
1027 =item spool_invoice [ TEMPLATENAME ]
1029 Spools this invoice data (see L<FS::spool_csv>)
1031 TEMPLATENAME is unused?
1037 my $template = scalar(@_) ? shift : '';
1040 'format' => $conf->config('cust_bill-spoolformat'),
1041 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1045 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1047 Like B<send>, but only sends the invoice if it is the newest open invoice for
1052 sub send_if_newest {
1057 grep { $_->owed > 0 }
1058 qsearch('cust_bill', {
1059 'custnum' => $self->custnum,
1060 #'_date' => { op=>'>', value=>$self->_date },
1061 'invnum' => { op=>'>', value=>$self->invnum },
1068 =item send_csv OPTION => VALUE, ...
1070 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1074 protocol - currently only "ftp"
1080 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1081 and YYMMDDHHMMSS is a timestamp.
1083 See L</print_csv> for a description of the output format.
1088 my($self, %opt) = @_;
1092 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1093 mkdir $spooldir, 0700 unless -d $spooldir;
1095 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1096 my $file = "$spooldir/$tracctnum.csv";
1098 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1100 open(CSV, ">$file") or die "can't open $file: $!";
1108 if ( $opt{protocol} eq 'ftp' ) {
1109 eval "use Net::FTP;";
1111 $net = Net::FTP->new($opt{server}) or die @$;
1113 die "unknown protocol: $opt{protocol}";
1116 $net->login( $opt{username}, $opt{password} )
1117 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1119 $net->binary or die "can't set binary mode";
1121 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1123 $net->put($file) or die "can't put $file: $!";
1133 Spools CSV invoice data.
1139 =item format - 'default' or 'billco'
1141 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1143 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1145 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1152 my($self, %opt) = @_;
1154 my $cust_main = $self->cust_main;
1156 if ( $opt{'dest'} ) {
1157 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1158 $cust_main->invoicing_list;
1159 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1160 || ! keys %invoicing_list;
1163 if ( $opt{'balanceover'} ) {
1165 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1168 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1169 mkdir $spooldir, 0700 unless -d $spooldir;
1171 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1175 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1176 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1179 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1181 open(CSV, ">>$file") or die "can't open $file: $!";
1182 flock(CSV, LOCK_EX);
1187 if ( lc($opt{'format'}) eq 'billco' ) {
1189 flock(CSV, LOCK_UN);
1194 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1197 open(CSV,">>$file") or die "can't open $file: $!";
1198 flock(CSV, LOCK_EX);
1204 flock(CSV, LOCK_UN);
1211 =item print_csv OPTION => VALUE, ...
1213 Returns CSV data for this invoice.
1217 format - 'default' or 'billco'
1219 Returns a list consisting of two scalars. The first is a single line of CSV
1220 header information for this invoice. The second is one or more lines of CSV
1221 detail information for this invoice.
1223 If I<format> is not specified or "default", the fields of the CSV file are as
1226 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1230 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1232 B<record_type> is C<cust_bill> for the initial header line only. The
1233 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1234 fields are filled in.
1236 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1237 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1240 =item invnum - invoice number
1242 =item custnum - customer number
1244 =item _date - invoice date
1246 =item charged - total invoice amount
1248 =item first - customer first name
1250 =item last - customer first name
1252 =item company - company name
1254 =item address1 - address line 1
1256 =item address2 - address line 1
1266 =item pkg - line item description
1268 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1270 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1272 =item sdate - start date for recurring fee
1274 =item edate - end date for recurring fee
1278 If I<format> is "billco", the fields of the header CSV file are as follows:
1280 +-------------------------------------------------------------------+
1281 | FORMAT HEADER FILE |
1282 |-------------------------------------------------------------------|
1283 | Field | Description | Name | Type | Width |
1284 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1285 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1286 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1287 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1288 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1289 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1290 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1291 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1292 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1293 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1294 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1295 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1296 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1297 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1298 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1299 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1300 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1301 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1302 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1303 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1304 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1305 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1306 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1307 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1308 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1309 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1310 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1311 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1312 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1313 +-------+-------------------------------+------------+------+-------+
1315 If I<format> is "billco", the fields of the detail CSV file are as follows:
1317 FORMAT FOR DETAIL FILE
1319 Field | Description | Name | Type | Width
1320 1 | N/A-Leave Empty | RC | CHAR | 2
1321 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1322 3 | Account Number | TRACCTNUM | CHAR | 15
1323 4 | Invoice Number | TRINVOICE | CHAR | 15
1324 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1325 6 | Transaction Detail | DETAILS | CHAR | 100
1326 7 | Amount | AMT | NUM* | 9
1327 8 | Line Format Control** | LNCTRL | CHAR | 2
1328 9 | Grouping Code | GROUP | CHAR | 2
1329 10 | User Defined | ACCT CODE | CHAR | 15
1334 my($self, %opt) = @_;
1336 eval "use Text::CSV_XS";
1339 my $cust_main = $self->cust_main;
1341 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1343 if ( lc($opt{'format'}) eq 'billco' ) {
1346 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1348 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1350 my( $previous_balance, @unused ) = $self->previous; #previous balance
1352 my $pmt_cr_applied = 0;
1353 $pmt_cr_applied += $_->{'amount'}
1354 foreach ( $self->_items_payments, $self->_items_credits ) ;
1356 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1359 '', # 1 | N/A-Leave Empty CHAR 2
1360 '', # 2 | N/A-Leave Empty CHAR 15
1361 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1362 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1363 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1364 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1365 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1366 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1367 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1368 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1369 '', # 10 | Ancillary Billing Information CHAR 30
1370 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1371 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1374 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1377 $duedate, # 14 | Bill Due Date CHAR 10
1379 $previous_balance, # 15 | Previous Balance NUM* 9
1380 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1381 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1382 $totaldue, # 18 | Total Amt Due NUM* 9
1383 $totaldue, # 19 | Total Amt Due NUM* 9
1384 '', # 20 | 30 Day Aging NUM* 9
1385 '', # 21 | 60 Day Aging NUM* 9
1386 '', # 22 | 90 Day Aging NUM* 9
1387 'N', # 23 | Y/N CHAR 1
1388 '', # 24 | Remittance automation CHAR 100
1389 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1390 $self->custnum, # 26 | Customer Reference Number CHAR 15
1391 '0', # 27 | Federal Tax*** NUM* 9
1392 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1393 '0', # 29 | Other Taxes & Fees*** NUM* 9
1402 time2str("%x", $self->_date),
1403 sprintf("%.2f", $self->charged),
1404 ( map { $cust_main->getfield($_) }
1405 qw( first last company address1 address2 city state zip country ) ),
1407 ) or die "can't create csv";
1410 my $header = $csv->string. "\n";
1413 if ( lc($opt{'format'}) eq 'billco' ) {
1416 foreach my $item ( $self->_items_pkg ) {
1419 '', # 1 | N/A-Leave Empty CHAR 2
1420 '', # 2 | N/A-Leave Empty CHAR 15
1421 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1422 $self->invnum, # 4 | Invoice Number CHAR 15
1423 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1424 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1425 $item->{'amount'}, # 7 | Amount NUM* 9
1426 '', # 8 | Line Format Control** CHAR 2
1427 '', # 9 | Grouping Code CHAR 2
1428 '', # 10 | User Defined CHAR 15
1431 $detail .= $csv->string. "\n";
1437 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1439 my($pkg, $setup, $recur, $sdate, $edate);
1440 if ( $cust_bill_pkg->pkgnum ) {
1442 ($pkg, $setup, $recur, $sdate, $edate) = (
1443 $cust_bill_pkg->part_pkg->pkg,
1444 ( $cust_bill_pkg->setup != 0
1445 ? sprintf("%.2f", $cust_bill_pkg->setup )
1447 ( $cust_bill_pkg->recur != 0
1448 ? sprintf("%.2f", $cust_bill_pkg->recur )
1450 ( $cust_bill_pkg->sdate
1451 ? time2str("%x", $cust_bill_pkg->sdate)
1453 ($cust_bill_pkg->edate
1454 ?time2str("%x", $cust_bill_pkg->edate)
1458 } else { #pkgnum tax
1459 next unless $cust_bill_pkg->setup != 0;
1460 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1461 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1463 ($pkg, $setup, $recur, $sdate, $edate) =
1464 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1470 ( map { '' } (1..11) ),
1471 ($pkg, $setup, $recur, $sdate, $edate)
1472 ) or die "can't create csv";
1474 $detail .= $csv->string. "\n";
1480 ( $header, $detail );
1486 Pays this invoice with a compliemntary payment. If there is an error,
1487 returns the error, otherwise returns false.
1493 my $cust_pay = new FS::cust_pay ( {
1494 'invnum' => $self->invnum,
1495 'paid' => $self->owed,
1498 'payinfo' => $self->cust_main->payinfo,
1506 Attempts to pay this invoice with a credit card payment via a
1507 Business::OnlinePayment realtime gateway. See
1508 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1509 for supported processors.
1515 $self->realtime_bop( 'CC', @_ );
1520 Attempts to pay this invoice with an electronic check (ACH) payment via a
1521 Business::OnlinePayment realtime gateway. See
1522 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1523 for supported processors.
1529 $self->realtime_bop( 'ECHECK', @_ );
1534 Attempts to pay this invoice with phone bill (LEC) payment via a
1535 Business::OnlinePayment realtime gateway. See
1536 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1537 for supported processors.
1543 $self->realtime_bop( 'LEC', @_ );
1547 my( $self, $method ) = @_;
1549 my $cust_main = $self->cust_main;
1550 my $balance = $cust_main->balance;
1551 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1552 $amount = sprintf("%.2f", $amount);
1553 return "not run (balance $balance)" unless $amount > 0;
1555 my $description = 'Internet Services';
1556 if ( $conf->exists('business-onlinepayment-description') ) {
1557 my $dtempl = $conf->config('business-onlinepayment-description');
1559 my $agent_obj = $cust_main->agent
1560 or die "can't retreive agent for $cust_main (agentnum ".
1561 $cust_main->agentnum. ")";
1562 my $agent = $agent_obj->agent;
1563 my $pkgs = join(', ',
1564 map { $_->part_pkg->pkg }
1565 grep { $_->pkgnum } $self->cust_bill_pkg
1567 $description = eval qq("$dtempl");
1570 $cust_main->realtime_bop($method, $amount,
1571 'description' => $description,
1572 'invnum' => $self->invnum,
1577 =item batch_card OPTION => VALUE...
1579 Adds a payment for this invoice to the pending credit card batch (see
1580 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1581 runs the payment using a realtime gateway.
1586 my ($self, %options) = @_;
1587 my $cust_main = $self->cust_main;
1589 $options{invnum} = $self->invnum;
1591 $cust_main->batch_card(%options);
1594 sub _agent_template {
1596 $self->cust_main->agent_template;
1599 sub _agent_invoice_from {
1601 $self->cust_main->agent_invoice_from;
1604 =item print_text [ TIME [ , TEMPLATE ] ]
1606 Returns an text invoice, as a list of lines.
1608 TIME an optional value used to control the printing of overdue messages. The
1609 default is now. It isn't the date of the invoice; that's the `_date' field.
1610 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1611 L<Time::Local> and L<Date::Parse> for conversion functions.
1616 my( $self, $today, $template ) = @_;
1618 my %params = ( 'format' => 'template' );
1619 $params{'time'} = $today if $today;
1620 $params{'template'} = $template if $template;
1622 $self->print_generic( %params );
1625 =item print_latex [ TIME [ , TEMPLATE ] ]
1627 Internal method - returns a filename of a filled-in LaTeX template for this
1628 invoice (Note: add ".tex" to get the actual filename), and a filename of
1629 an associated logo (with the .eps extension included).
1631 See print_ps and print_pdf for methods that return PostScript and PDF output.
1633 TIME an optional value used to control the printing of overdue messages. The
1634 default is now. It isn't the date of the invoice; that's the `_date' field.
1635 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1636 L<Time::Local> and L<Date::Parse> for conversion functions.
1641 my( $self, $today, $template ) = @_;
1643 my %params = ( 'format' => 'latex' );
1644 $params{'time'} = $today if $today;
1645 $params{'template'} = $template if $template;
1647 $template ||= $self->_agent_template;
1649 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1650 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1654 ) or die "can't open temp file: $!\n";
1656 my $agentnum = $self->cust_main->agentnum;
1658 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1659 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1660 or die "can't write temp file: $!\n";
1662 print $lh $conf->config_binary('logo.eps', $agentnum)
1663 or die "can't write temp file: $!\n";
1666 $params{'logo_file'} = $lh->filename;
1668 my @filled_in = $self->print_generic( %params );
1670 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1674 ) or die "can't open temp file: $!\n";
1675 print $fh join('', @filled_in );
1678 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1679 return ($1, $params{'logo_file'});
1683 =item print_generic OPTIONS_HASH
1685 Internal method - returns a filled-in template for this invoice as a scalar.
1687 See print_ps and print_pdf for methods that return PostScript and PDF output.
1689 Non optional options include
1690 format - latex, html, template
1692 Optional options include
1694 template - a value used as a suffix for a configuration template
1696 time - a value used to control the printing of overdue messages. The
1697 default is now. It isn't the date of the invoice; that's the `_date' field.
1698 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1699 L<Time::Local> and L<Date::Parse> for conversion functions.
1703 unsquelch_cdr - overrides any per customer cdr squelching when true
1707 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1708 # (alignment?) problems to change them all to '%.2f' ?
1711 my( $self, %params ) = @_;
1712 my $today = $params{today} ? $params{today} : time;
1713 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1716 my $format = $params{format};
1717 die "Unknown format: $format"
1718 unless $format =~ /^(latex|html|template)$/;
1720 my $cust_main = $self->cust_main;
1721 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1722 unless $cust_main->payname
1723 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1725 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1726 'html' => [ '<%=', '%>' ],
1727 'template' => [ '{', '}' ],
1730 #create the template
1731 my $template = $params{template} ? $params{template} : $self->_agent_template;
1732 my $templatefile = "invoice_$format";
1733 $templatefile .= "_$template"
1734 if length($template);
1735 my @invoice_template = map "$_\n", $conf->config($templatefile)
1736 or die "cannot load config data $templatefile";
1739 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1740 #change this to a die when the old code is removed
1741 warn "old-style invoice template $templatefile; ".
1742 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1743 $old_latex = 'true';
1744 @invoice_template = _translate_old_latex_format(@invoice_template);
1747 my $text_template = new Text::Template(
1749 SOURCE => \@invoice_template,
1750 DELIMITERS => $delimiters{$format},
1753 $text_template->compile()
1754 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1757 # additional substitution could possibly cause breakage in existing templates
1758 my %convert_maps = (
1760 'notes' => sub { map "$_", @_ },
1761 'footer' => sub { map "$_", @_ },
1762 'smallfooter' => sub { map "$_", @_ },
1763 'returnaddress' => sub { map "$_", @_ },
1764 'coupon' => sub { map "$_", @_ },
1770 s/%%(.*)$/<!-- $1 -->/g;
1771 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1772 s/\\begin\{enumerate\}/<ol>/g;
1774 s/\\end\{enumerate\}/<\/ol>/g;
1775 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1784 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1786 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1791 s/\\\\\*?\s*$/<BR>/;
1792 s/\\hyphenation\{[\w\s\-]+}//;
1797 'coupon' => sub { "" },
1804 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1805 s/\\begin\{enumerate\}//g;
1807 s/\\end\{enumerate\}//g;
1808 s/\\textbf\{(.*)\}/$1/g;
1815 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1817 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1822 s/\\\\\*?\s*$/\n/; # dubious
1823 s/\\hyphenation\{[\w\s\-]+}//;
1827 'coupon' => sub { "" },
1832 # hashes for differing output formats
1833 my %nbsps = ( 'latex' => '~',
1834 'html' => '', # '&nbps;' would be nice
1835 'template' => '', # not used
1837 my $nbsp = $nbsps{$format};
1839 my %escape_functions = ( 'latex' => \&_latex_escape,
1840 'html' => \&encode_entities,
1841 'template' => sub { shift },
1843 my $escape_function = $escape_functions{$format};
1845 my %date_formats = ( 'latex' => '%b %o, %Y',
1846 'html' => '%b %o, %Y',
1849 my $date_format = $date_formats{$format};
1851 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1853 'html' => sub { return '<b>'. shift(). '</b>'
1855 'template' => sub { shift },
1857 my $embolden_function = $embolden_functions{$format};
1860 # generate template variables
1863 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1867 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1873 $returnaddress = join("\n",
1874 $conf->config_orbase("invoice_${format}returnaddress", $template)
1877 } elsif ( grep /\S/,
1878 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1880 my $convert_map = $convert_maps{$format}{'returnaddress'};
1883 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1888 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1890 my $convert_map = $convert_maps{$format}{'returnaddress'};
1891 $returnaddress = join( "\n", &$convert_map(
1892 map { s/( {2,})/'~' x length($1)/eg;
1896 ( $conf->config('company_name', $self->cust_main->agentnum),
1897 $conf->config('company_address', $self->cust_main->agentnum),
1904 my $warning = "Couldn't find a return address; ".
1905 "do you need to set the company_address configuration value?";
1907 $returnaddress = $nbsp;
1908 #$returnaddress = $warning;
1912 my %invoice_data = (
1913 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
1914 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
1915 'custnum' => $cust_main->display_custnum,
1916 'invnum' => $self->invnum,
1917 'date' => time2str($date_format, $self->_date),
1918 'today' => time2str('%b %o, %Y', $today),
1919 'agent' => &$escape_function($cust_main->agent->agent),
1920 'agent_custid' => &$escape_function($cust_main->agent_custid),
1921 'payname' => &$escape_function($cust_main->payname),
1922 'company' => &$escape_function($cust_main->company),
1923 'address1' => &$escape_function($cust_main->address1),
1924 'address2' => &$escape_function($cust_main->address2),
1925 'city' => &$escape_function($cust_main->city),
1926 'state' => &$escape_function($cust_main->state),
1927 'zip' => &$escape_function($cust_main->zip),
1928 'fax' => &$escape_function($cust_main->fax),
1929 'returnaddress' => $returnaddress,
1931 'terms' => $self->terms,
1932 'template' => $template, #params{'template'},
1933 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1934 # better hang on to conf_dir for a while
1935 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1938 'current_charges' => sprintf("%.2f", $self->charged),
1939 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1940 'ship_enable' => $conf->exists('invoice-ship_address'),
1941 'unitprices' => $conf->exists('invoice-unitprice'),
1944 my $countrydefault = $conf->config('countrydefault') || 'US';
1945 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1946 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1947 my $method = $prefix.$_;
1948 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1950 $invoice_data{'ship_country'} = ''
1951 if ( $invoice_data{'ship_country'} eq $countrydefault );
1953 $invoice_data{'cid'} = $params{'cid'}
1956 if ( $cust_main->country eq $countrydefault ) {
1957 $invoice_data{'country'} = '';
1959 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1963 $invoice_data{'address'} = \@address;
1965 $cust_main->payname.
1966 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1967 ? " (P.O. #". $cust_main->payinfo. ")"
1971 push @address, $cust_main->company
1972 if $cust_main->company;
1973 push @address, $cust_main->address1;
1974 push @address, $cust_main->address2
1975 if $cust_main->address2;
1977 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1978 push @address, $invoice_data{'country'}
1979 if $invoice_data{'country'};
1981 while (scalar(@address) < 5);
1983 $invoice_data{'logo_file'} = $params{'logo_file'}
1984 if $params{'logo_file'};
1986 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1987 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1988 #my $balance_due = $self->owed + $pr_total - $cr_total;
1989 my $balance_due = $self->owed + $pr_total;
1990 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
1991 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
1993 my $agentnum = $self->cust_main->agentnum;
1995 #do variable substitution in notes, footer, smallfooter
1996 foreach my $include (qw( notes footer smallfooter coupon )) {
1998 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2001 if ( $conf->exists($inc_file, $agentnum)
2002 && length( $conf->config($inc_file, $agentnum) ) ) {
2004 @inc_src = $conf->config($inc_file, $agentnum);
2008 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2010 my $convert_map = $convert_maps{$format}{$include};
2012 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2013 s/--\@\]/$delimiters{$format}[1]/g;
2016 &$convert_map( $conf->config($inc_file, $agentnum) );
2020 my $inc_tt = new Text::Template (
2022 SOURCE => [ map "$_\n", @inc_src ],
2023 DELIMITERS => $delimiters{$format},
2024 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2026 unless ( $inc_tt->compile() ) {
2027 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2028 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2032 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2034 $invoice_data{$include} =~ s/\n+$//
2035 if ($format eq 'latex');
2038 $invoice_data{'po_line'} =
2039 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2040 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2043 my %money_chars = ( 'latex' => '',
2044 'html' => $conf->config('money_char') || '$',
2047 my $money_char = $money_chars{$format};
2049 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2050 'html' => $conf->config('money_char') || '$',
2053 my $other_money_char = $other_money_chars{$format};
2055 my @detail_items = ();
2056 my @total_items = ();
2060 $invoice_data{'detail_items'} = \@detail_items;
2061 $invoice_data{'total_items'} = \@total_items;
2062 $invoice_data{'buf'} = \@buf;
2063 $invoice_data{'sections'} = \@sections;
2065 my $previous_section = { 'description' => 'Previous Charges',
2066 'subtotal' => $other_money_char.
2067 sprintf('%.2f', $pr_total),
2071 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2072 'subtotal' => $taxtotal }; # adjusted below
2074 my $adjusttotal = 0;
2075 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2076 'subtotal' => 0 }; # adjusted below
2078 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2079 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2080 my $late_sections = [];
2081 if ( $multisection ) {
2082 push @sections, $self->_items_sections( $late_sections );
2084 push @sections, { 'description' => '', 'subtotal' => '' };
2087 unless ( $conf->exists('disable_previous_balance')
2088 || $conf->exists('previous_balance-summary_only')
2092 foreach my $line_item ( $self->_items_previous ) {
2095 ext_description => [],
2097 $detail->{'ref'} = $line_item->{'pkgnum'};
2098 $detail->{'quantity'} = 1;
2099 $detail->{'section'} = $previous_section;
2100 $detail->{'description'} = &$escape_function($line_item->{'description'});
2101 if ( exists $line_item->{'ext_description'} ) {
2102 @{$detail->{'ext_description'}} = map {
2103 &$escape_function($_);
2104 } @{$line_item->{'ext_description'}};
2106 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2107 $line_item->{'amount'};
2108 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2110 push @detail_items, $detail;
2111 push @buf, [ $detail->{'description'},
2112 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2118 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2119 push @buf, ['','-----------'];
2120 push @buf, [ 'Total Previous Balance',
2121 $money_char. sprintf("%10.2f", $pr_total) ];
2125 foreach my $section (@sections, @$late_sections) {
2127 $section->{'subtotal'} = $other_money_char.
2128 sprintf('%.2f', $section->{'subtotal'})
2131 if ( $section->{'description'} ) {
2132 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2138 $options{'section'} = $section if $multisection;
2139 $options{'format'} = $format;
2140 $options{'escape_function'} = $escape_function;
2141 $options{'format_function'} = sub { () } unless $unsquelched;
2142 $options{'unsquelched'} = $unsquelched;
2144 foreach my $line_item ( $self->_items_pkg(%options) ) {
2146 ext_description => [],
2148 $detail->{'ref'} = $line_item->{'pkgnum'};
2149 $detail->{'quantity'} = $line_item->{'quantity'};
2150 $detail->{'section'} = $section;
2151 $detail->{'description'} = &$escape_function($line_item->{'description'});
2152 if ( exists $line_item->{'ext_description'} ) {
2153 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2155 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2156 $line_item->{'amount'};
2157 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2158 $line_item->{'unit_amount'};
2159 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2161 push @detail_items, $detail;
2162 push @buf, ( [ $detail->{'description'},
2163 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2165 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2169 if ( $section->{'description'} ) {
2170 push @buf, ( ['','-----------'],
2171 [ $section->{'description'}. ' sub-total',
2172 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2181 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2182 unshift @sections, $previous_section if $pr_total;
2185 foreach my $tax ( $self->_items_tax ) {
2187 $taxtotal += $tax->{'amount'};
2189 my $description = &$escape_function( $tax->{'description'} );
2190 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2192 if ( $multisection ) {
2194 my $money = $old_latex ? '' : $money_char;
2195 push @detail_items, {
2196 ext_description => [],
2199 description => $description,
2200 amount => $money. $amount,
2202 section => $tax_section,
2207 push @total_items, {
2208 'total_item' => $description,
2209 'total_amount' => $other_money_char. $amount,
2214 push @buf,[ $description,
2215 $money_char. $amount,
2222 $total->{'total_item'} = 'Sub-total';
2223 $total->{'total_amount'} =
2224 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2226 if ( $multisection ) {
2227 $tax_section->{'subtotal'} = $other_money_char.
2228 sprintf('%.2f', $taxtotal);
2229 $tax_section->{'pretotal'} = 'New charges sub-total '.
2230 $total->{'total_amount'};
2231 push @sections, $tax_section if $taxtotal;
2233 unshift @total_items, $total;
2236 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2238 push @buf,['','-----------'];
2239 push @buf,[( $conf->exists('disable_previous_balance')
2241 : 'Total New Charges'
2243 $money_char. sprintf("%10.2f",$self->charged) ];
2248 $total->{'total_item'} = &$embolden_function('Total');
2249 $total->{'total_amount'} =
2250 &$embolden_function(
2253 $self->charged + ( $conf->exists('disable_previous_balance')
2259 if ( $multisection ) {
2260 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2261 sprintf('%.2f', $self->charged );
2263 push @total_items, $total;
2265 push @buf,['','-----------'];
2266 push @buf,['Total Charges',
2268 sprintf( '%10.2f', $self->charged +
2269 ( $conf->exists('disable_previous_balance')
2278 unless ( $conf->exists('disable_previous_balance') ) {
2279 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2282 my $credittotal = 0;
2283 foreach my $credit ( $self->_items_credits ) {
2285 $total->{'total_item'} = &$escape_function($credit->{'description'});
2286 $credittotal += $credit->{'amount'};
2287 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2288 $adjusttotal += $credit->{'amount'};
2289 if ( $multisection ) {
2290 my $money = $old_latex ? '' : $money_char;
2291 push @detail_items, {
2292 ext_description => [],
2295 description => &$escape_function($credit->{'description'}),
2296 amount => $money. $credit->{'amount'},
2298 section => $adjust_section,
2301 push @total_items, $total;
2304 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2307 foreach ( $self->cust_credited ) {
2309 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2311 my $reason = substr($_->cust_credit->reason,0,32);
2312 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
2313 $reason = " ($reason) " if $reason;
2315 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". $reason,
2316 $money_char. sprintf("%10.2f",$_->amount)
2321 my $paymenttotal = 0;
2322 foreach my $payment ( $self->_items_payments ) {
2324 $total->{'total_item'} = &$escape_function($payment->{'description'});
2325 $paymenttotal += $payment->{'amount'};
2326 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2327 $adjusttotal += $payment->{'amount'};
2328 if ( $multisection ) {
2329 my $money = $old_latex ? '' : $money_char;
2330 push @detail_items, {
2331 ext_description => [],
2334 description => &$escape_function($payment->{'description'}),
2335 amount => $money. $payment->{'amount'},
2337 section => $adjust_section,
2340 push @total_items, $total;
2342 push @buf, [ $payment->{'description'},
2343 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2346 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2348 if ( $multisection ) {
2349 $adjust_section->{'subtotal'} = $other_money_char.
2350 sprintf('%.2f', $adjusttotal);
2351 push @sections, $adjust_section;
2356 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2357 $total->{'total_amount'} =
2358 &$embolden_function(
2359 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2361 if ( $multisection ) {
2362 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2363 $total->{'total_amount'};
2365 push @total_items, $total;
2367 push @buf,['','-----------'];
2368 push @buf,[$self->balance_due_msg, $money_char.
2369 sprintf("%10.2f", $balance_due ) ];
2373 if ( $multisection ) {
2374 push @sections, @$late_sections
2380 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2381 /invoice_lines\((\d*)\)/;
2382 $invoice_lines += $1 || scalar(@buf);
2385 die "no invoice_lines() functions in template?"
2386 if ( $format eq 'template' && !$wasfunc );
2388 if ($format eq 'template') {
2390 if ( $invoice_lines ) {
2391 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2392 $invoice_data{'total_pages'}++
2393 if scalar(@buf) % $invoice_lines;
2396 #setup subroutine for the template
2397 sub FS::cust_bill::_template::invoice_lines {
2398 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2400 scalar(@FS::cust_bill::_template::buf)
2401 ? shift @FS::cust_bill::_template::buf
2410 push @collect, split("\n",
2411 $text_template->fill_in( HASH => \%invoice_data,
2412 PACKAGE => 'FS::cust_bill::_template'
2415 $FS::cust_bill::_template::page++;
2417 map "$_\n", @collect;
2419 warn "filling in template for invoice ". $self->invnum. "\n"
2421 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2424 $text_template->fill_in(HASH => \%invoice_data);
2428 =item print_ps [ TIME [ , TEMPLATE ] ]
2430 Returns an postscript invoice, as a scalar.
2432 TIME an optional value used to control the printing of overdue messages. The
2433 default is now. It isn't the date of the invoice; that's the `_date' field.
2434 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2435 L<Time::Local> and L<Date::Parse> for conversion functions.
2442 my ($file, $lfile) = $self->print_latex(@_);
2443 my $ps = generate_ps($file);
2449 =item print_pdf [ TIME [ , TEMPLATE ] ]
2451 Returns an PDF invoice, as a scalar.
2453 TIME an optional value used to control the printing of overdue messages. The
2454 default is now. It isn't the date of the invoice; that's the `_date' field.
2455 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2456 L<Time::Local> and L<Date::Parse> for conversion functions.
2463 my ($file, $lfile) = $self->print_latex(@_);
2464 my $pdf = generate_pdf($file);
2470 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2472 Returns an HTML invoice, as a scalar.
2474 TIME an optional value used to control the printing of overdue messages. The
2475 default is now. It isn't the date of the invoice; that's the `_date' field.
2476 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2477 L<Time::Local> and L<Date::Parse> for conversion functions.
2479 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2480 when emailing the invoice as part of a multipart/related MIME email.
2488 %params = %{ shift() };
2490 $params{'time'} = shift;
2491 $params{'template'} = shift;
2492 $params{'cid'} = shift;
2495 $params{'format'} = 'html';
2497 $self->print_generic( %params );
2500 # quick subroutine for print_latex
2502 # There are ten characters that LaTeX treats as special characters, which
2503 # means that they do not simply typeset themselves:
2504 # # $ % & ~ _ ^ \ { }
2506 # TeX ignores blanks following an escaped character; if you want a blank (as
2507 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2511 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2512 $value =~ s/([<>])/\$$1\$/g;
2516 #utility methods for print_*
2518 sub _translate_old_latex_format {
2519 warn "_translate_old_latex_format called\n"
2526 if ( $line =~ /^%%Detail\s*$/ ) {
2528 push @template, q![@--!,
2529 q! foreach my $_tr_line (@detail_items) {!,
2530 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2531 q! $_tr_line->{'description'} .= !,
2532 q! "\\tabularnewline\n~~".!,
2533 q! join( "\\tabularnewline\n~~",!,
2534 q! @{$_tr_line->{'ext_description'}}!,
2538 while ( ( my $line_item_line = shift )
2539 !~ /^%%EndDetail\s*$/ ) {
2540 $line_item_line =~ s/'/\\'/g; # nice LTS
2541 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2542 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2543 push @template, " \$OUT .= '$line_item_line';";
2546 push @template, '}',
2549 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2551 push @template, '[@--',
2552 ' foreach my $_tr_line (@total_items) {';
2554 while ( ( my $total_item_line = shift )
2555 !~ /^%%EndTotalDetails\s*$/ ) {
2556 $total_item_line =~ s/'/\\'/g; # nice LTS
2557 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2558 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2559 push @template, " \$OUT .= '$total_item_line';";
2562 push @template, '}',
2566 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2567 push @template, $line;
2573 warn "$_\n" foreach @template;
2582 #check for an invoice- specific override (eventually)
2584 #check for a customer- specific override
2585 return $self->cust_main->invoice_terms
2586 if $self->cust_main->invoice_terms;
2588 #use configured default or default default
2589 $conf->config('invoice_default_terms') || 'Payable upon receipt';
2595 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2596 $duedate = $self->_date() + ( $1 * 86400 );
2603 $self->due_date ? time2str(shift, $self->due_date) : '';
2606 sub balance_due_msg {
2608 my $msg = 'Balance Due';
2609 return $msg unless $self->terms;
2610 if ( $self->due_date ) {
2611 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2612 } elsif ( $self->terms ) {
2613 $msg .= ' - '. $self->terms;
2618 sub balance_due_date {
2621 if ( $conf->exists('invoice_default_terms')
2622 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2623 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2628 =item invnum_date_pretty
2630 Returns a string with the invoice number and date, for example:
2631 "Invoice #54 (3/20/2008)"
2635 sub invnum_date_pretty {
2637 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2642 Returns a string with the date, for example: "3/20/2008"
2648 time2str('%x', $self->_date);
2651 sub _items_sections {
2658 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2661 if ( $cust_bill_pkg->pkgnum > 0 ) {
2662 my $usage = $cust_bill_pkg->usage;
2664 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2665 my $desc = $display->section;
2666 my $type = $display->type;
2668 if ( $display->post_total ) {
2669 if (! $type || $type eq 'S') {
2670 $l{$desc} += $cust_bill_pkg->setup
2671 if ( $cust_bill_pkg->setup != 0 );
2675 $l{$desc} += $cust_bill_pkg->recur
2676 if ( $cust_bill_pkg->recur != 0 );
2679 if ($type && $type eq 'R') {
2680 $l{$desc} += $cust_bill_pkg->recur - $usage
2681 if ( $cust_bill_pkg->recur != 0 );
2684 if ($type && $type eq 'U') {
2685 $l{$desc} += $usage;
2689 if (! $type || $type eq 'S') {
2690 $s{$desc} += $cust_bill_pkg->setup
2691 if ( $cust_bill_pkg->setup != 0 );
2695 $s{$desc} += $cust_bill_pkg->recur
2696 if ( $cust_bill_pkg->recur != 0 );
2699 if ($type && $type eq 'R') {
2700 $s{$desc} += $cust_bill_pkg->recur - $usage
2701 if ( $cust_bill_pkg->recur != 0 );
2704 if ($type && $type eq 'U') {
2705 $s{$desc} += $usage;
2716 push @$late, map { { 'description' => $_,
2717 'subtotal' => $l{$_},
2721 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2728 #my @display = scalar(@_)
2730 # : qw( _items_previous _items_pkg );
2731 # #: qw( _items_pkg );
2732 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2733 my @display = qw( _items_previous _items_pkg );
2736 foreach my $display ( @display ) {
2737 push @b, $self->$display(@_);
2742 sub _items_previous {
2744 my $cust_main = $self->cust_main;
2745 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2747 foreach ( @pr_cust_bill ) {
2749 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2750 ' ('. time2str('%x',$_->_date). ')',
2751 #'pkgpart' => 'N/A',
2753 'amount' => sprintf("%.2f", $_->owed),
2759 # 'description' => 'Previous Balance',
2760 # #'pkgpart' => 'N/A',
2761 # 'pkgnum' => 'N/A',
2762 # 'amount' => sprintf("%10.2f", $pr_total ),
2763 # 'ext_description' => [ map {
2764 # "Invoice ". $_->invnum.
2765 # " (". time2str("%x",$_->_date). ") ".
2766 # sprintf("%10.2f", $_->owed)
2767 # } @pr_cust_bill ],
2774 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2775 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2779 return 0 unless $a cmp $b;
2780 return -1 if $b eq 'Tax';
2781 return 1 if $a eq 'Tax';
2782 return -1 if $b eq 'Other surcharges';
2783 return 1 if $a eq 'Other surcharges';
2789 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2790 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2793 sub _items_cust_bill_pkg {
2795 my $cust_bill_pkg = shift;
2798 my $format = $opt{format} || '';
2799 my $escape_function = $opt{escape_function} || sub { shift };
2800 my $format_function = $opt{format_function} || '';
2801 my $unsquelched = $opt{unsquelched} || '';
2802 my $section = $opt{section}->{description} if $opt{section};
2805 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2807 foreach my $display ( grep { defined($section)
2808 ? $_->section eq $section
2811 $cust_bill_pkg->cust_bill_pkg_display
2815 my $type = $display->type;
2817 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2819 my $desc = $cust_bill_pkg->desc;
2820 $desc = substr($desc, 0, 50). '...'
2821 if $format eq 'latex' && length($desc) > 50;
2823 my %details_opt = ( 'format' => $format,
2824 'escape_function' => $escape_function,
2825 'format_function' => $format_function,
2828 if ( $cust_bill_pkg->pkgnum > 0 ) {
2830 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2832 my $description = $desc;
2833 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2836 push @d, map &{$escape_function}($_),
2837 $cust_pkg->h_labels_short($self->_date)
2838 unless $cust_pkg->part_pkg->hide_svc_detail;
2839 push @d, $cust_bill_pkg->details(%details_opt)
2840 if $cust_bill_pkg->recur == 0;
2843 description => $description,
2844 #pkgpart => $part_pkg->pkgpart,
2845 pkgnum => $cust_bill_pkg->pkgnum,
2846 amount => sprintf("%.2f", $cust_bill_pkg->setup),
2847 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitsetup),
2848 quantity => $cust_bill_pkg->quantity,
2849 ext_description => \@d,
2854 if ( $cust_bill_pkg->recur != 0 &&
2855 ( !$type || $type eq 'R' || $type eq 'U' )
2859 my $is_summary = $display->summary;
2860 my $description = $is_summary ? "Usage charges" : $desc;
2862 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2863 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2864 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2869 #at least until cust_bill_pkg has "past" ranges in addition to
2870 #the "future" sdate/edate ones... see #3032
2871 push @d, map &{$escape_function}($_),
2872 $cust_pkg->h_labels_short($self->_date)
2873 #$cust_bill_pkg->edate,
2874 #$cust_bill_pkg->sdate)
2875 unless $cust_pkg->part_pkg->hide_svc_detail
2876 || $cust_bill_pkg->itemdesc
2879 push @d, $cust_bill_pkg->details(%details_opt)
2880 unless ($is_summary || $type && $type eq 'R');
2884 $amount = $cust_bill_pkg->recur;
2885 }elsif($type eq 'R') {
2886 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2887 }elsif($type eq 'U') {
2888 $amount = $cust_bill_pkg->usage;
2892 description => $description,
2893 #pkgpart => $part_pkg->pkgpart,
2894 pkgnum => $cust_bill_pkg->pkgnum,
2895 amount => sprintf("%.2f", $amount),
2896 unit_amount => sprintf("%.2f", $cust_bill_pkg->unitrecur),
2897 quantity => $cust_bill_pkg->quantity,
2898 ext_description => \@d,
2899 } unless ( $type eq 'U' && ! $amount );
2903 } else { #pkgnum tax or one-shot line item (??)
2905 if ( $cust_bill_pkg->setup != 0 ) {
2907 'description' => $desc,
2908 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2911 if ( $cust_bill_pkg->recur != 0 ) {
2913 'description' => "$desc (".
2914 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2915 time2str("%x", $cust_bill_pkg->edate). ')',
2916 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2930 sub _items_credits {
2935 foreach ( $self->cust_credited ) {
2937 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2939 my $reason = $_->cust_credit->reason;
2940 #my $reason = substr($_->cust_credit->reason,0,32);
2941 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2942 $reason = " ($reason) " if $reason;
2944 #'description' => 'Credit ref\#'. $_->crednum.
2945 # " (". time2str("%x",$_->cust_credit->_date) .")".
2947 'description' => 'Credit applied '.
2948 time2str("%x",$_->cust_credit->_date). $reason,
2949 'amount' => sprintf("%.2f",$_->amount),
2952 #foreach ( @cr_cust_credit ) {
2954 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2955 # $money_char. sprintf("%10.2f",$_->credited)
2963 sub _items_payments {
2967 #get & print payments
2968 foreach ( $self->cust_bill_pay ) {
2970 #something more elaborate if $_->amount ne ->cust_pay->paid ?
2973 'description' => "Payment received ".
2974 time2str("%x",$_->cust_pay->_date ),
2975 'amount' => sprintf("%.2f", $_->amount )
2990 =item process_reprint
2994 sub process_reprint {
2995 process_re_X('print', @_);
2998 =item process_reemail
3002 sub process_reemail {
3003 process_re_X('email', @_);
3011 process_re_X('fax', @_);
3019 process_re_X('ftp', @_);
3026 sub process_respool {
3027 process_re_X('spool', @_);
3030 use Storable qw(thaw);
3034 my( $method, $job ) = ( shift, shift );
3035 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3037 my $param = thaw(decode_base64(shift));
3038 warn Dumper($param) if $DEBUG;
3049 my($method, $job, %param ) = @_;
3051 warn "re_X $method for job $job with param:\n".
3052 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3055 #some false laziness w/search/cust_bill.html
3057 my $orderby = 'ORDER BY cust_bill._date';
3059 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3061 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3063 my @cust_bill = qsearch( {
3064 #'select' => "cust_bill.*",
3065 'table' => 'cust_bill',
3066 'addl_from' => $addl_from,
3068 'extra_sql' => $extra_sql,
3069 'order_by' => $orderby,
3073 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3075 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3078 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3079 foreach my $cust_bill ( @cust_bill ) {
3080 $cust_bill->$method();
3082 if ( $job ) { #progressbar foo
3084 if ( time - $min_sec > $last ) {
3085 my $error = $job->update_statustext(
3086 int( 100 * $num / scalar(@cust_bill) )
3088 die $error if $error;
3099 =head1 CLASS METHODS
3105 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3111 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3116 Returns an SQL fragment to retreive the net amount (charged minus credited).
3122 'charged - '. $class->credited_sql;
3127 Returns an SQL fragment to retreive the amount paid against this invoice.
3133 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3134 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3139 Returns an SQL fragment to retreive the amount credited against this invoice.
3145 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3146 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3149 =item search_sql HASHREF
3151 Class method which returns an SQL WHERE fragment to search for parameters
3152 specified in HASHREF. Valid parameters are
3158 Epoch date (UNIX timestamp) setting a lower bound for _date values
3162 Epoch date (UNIX timestamp) setting an upper bound for _date values
3176 =item newest_percust
3180 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3185 my($class, $param) = @_;
3187 warn "$me search_sql called with params: \n".
3188 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3193 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3194 push @search, "cust_bill._date >= $1";
3196 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3197 push @search, "cust_bill._date < $1";
3199 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3200 push @search, "cust_bill.invnum >= $1";
3202 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3203 push @search, "cust_bill.invnum <= $1";
3205 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3206 push @search, "cust_main.agentnum = $1";
3209 push @search, '0 != '. FS::cust_bill->owed_sql
3210 if $param->{'open'};
3212 push @search, '0 != '. FS::cust_bill->net_sql
3215 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3216 if $param->{'days'};
3218 if ( $param->{'newest_percust'} ) {
3220 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3221 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3223 my @newest_where = map { my $x = $_;
3224 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3227 grep ! /^cust_main./, @search;
3228 my $newest_where = scalar(@newest_where)
3229 ? ' AND '. join(' AND ', @newest_where)
3233 push @search, "cust_bill._date = (
3234 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3235 WHERE newest_cust_bill.custnum = cust_bill.custnum
3241 my $curuser = $FS::CurrentUser::CurrentUser;
3242 if ( $curuser->username eq 'fs_queue'
3243 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3245 my $newuser = qsearchs('access_user', {
3246 'username' => $username,
3250 $curuser = $newuser;
3252 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3256 push @search, $curuser->agentnums_sql;
3258 join(' AND ', @search );
3270 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3271 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base