4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
9 use List::Util qw(min max);
11 use Text::Template 1.20;
13 use String::ShellQuote;
16 use Storable qw( freeze thaw );
18 use FS::UID qw( datasrc );
19 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
20 use FS::Record qw( qsearch qsearchs dbh );
21 use FS::cust_main_Mixin;
23 use FS::cust_statement;
24 use FS::cust_bill_pkg;
25 use FS::cust_bill_pkg_display;
26 use FS::cust_bill_pkg_detail;
30 use FS::cust_credit_bill;
32 use FS::cust_pay_batch;
33 use FS::cust_bill_event;
36 use FS::cust_bill_pay;
37 use FS::cust_bill_pay_batch;
38 use FS::part_bill_event;
41 use FS::cust_bill_batch;
42 use FS::cust_bill_pay_pkg;
43 use FS::cust_credit_bill_pkg;
45 @ISA = qw( FS::cust_main_Mixin FS::Record );
48 $me = '[FS::cust_bill]';
50 #ask FS::UID to run this stuff for us later
51 FS::UID->install_callback( sub {
53 $money_char = $conf->config('money_char') || '$';
54 $date_format = $conf->config('date_format') || '%x'; #/YY
55 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
56 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
61 FS::cust_bill - Object methods for cust_bill records
67 $record = new FS::cust_bill \%hash;
68 $record = new FS::cust_bill { 'column' => 'value' };
70 $error = $record->insert;
72 $error = $new_record->replace($old_record);
74 $error = $record->delete;
76 $error = $record->check;
78 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
80 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
82 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
84 @cust_pay_objects = $cust_bill->cust_pay;
86 $tax_amount = $record->tax;
88 @lines = $cust_bill->print_text;
89 @lines = $cust_bill->print_text $time;
93 An FS::cust_bill object represents an invoice; a declaration that a customer
94 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
95 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
96 following fields are currently supported:
102 =item invnum - primary key (assigned automatically for new invoices)
104 =item custnum - customer (see L<FS::cust_main>)
106 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
107 L<Time::Local> and L<Date::Parse> for conversion functions.
109 =item charged - amount of this invoice
111 =item invoice_terms - optional terms override for this specific invoice
115 Customer info at invoice generation time
119 =item previous_balance
121 =item billing_balance
129 =item printed - deprecated
137 =item closed - books closed flag, empty or `Y'
139 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
141 =item agent_invid - legacy invoice number
151 Creates a new invoice. To add the invoice to the database, see L<"insert">.
152 Invoices are normally created by calling the bill method of a customer object
153 (see L<FS::cust_main>).
157 sub table { 'cust_bill'; }
159 sub cust_linked { $_[0]->cust_main_custnum; }
160 sub cust_unlinked_msg {
162 "WARNING: can't find cust_main.custnum ". $self->custnum.
163 ' (cust_bill.invnum '. $self->invnum. ')';
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
175 warn "$me insert called\n" if $DEBUG;
177 local $SIG{HUP} = 'IGNORE';
178 local $SIG{INT} = 'IGNORE';
179 local $SIG{QUIT} = 'IGNORE';
180 local $SIG{TERM} = 'IGNORE';
181 local $SIG{TSTP} = 'IGNORE';
182 local $SIG{PIPE} = 'IGNORE';
184 my $oldAutoCommit = $FS::UID::AutoCommit;
185 local $FS::UID::AutoCommit = 0;
188 my $error = $self->SUPER::insert;
190 $dbh->rollback if $oldAutoCommit;
194 if ( $self->get('cust_bill_pkg') ) {
195 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
196 $cust_bill_pkg->invnum($self->invnum);
197 my $error = $cust_bill_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "can't create invoice line item: $error";
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212 This method now works but you probably shouldn't use it. Instead, apply a
213 credit against the invoice.
215 Using this method to delete invoices outright is really, really bad. There
216 would be no record you ever posted this invoice, and there are no check to
217 make sure charged = 0 or that there are no associated cust_bill_pkg records.
219 Really, don't use it.
225 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
227 local $SIG{HUP} = 'IGNORE';
228 local $SIG{INT} = 'IGNORE';
229 local $SIG{QUIT} = 'IGNORE';
230 local $SIG{TERM} = 'IGNORE';
231 local $SIG{TSTP} = 'IGNORE';
232 local $SIG{PIPE} = 'IGNORE';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
238 foreach my $table (qw(
251 foreach my $linked ( $self->$table() ) {
252 my $error = $linked->delete;
254 $dbh->rollback if $oldAutoCommit;
261 my $error = $self->SUPER::delete(@_);
263 $dbh->rollback if $oldAutoCommit;
267 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
273 =item replace [ OLD_RECORD ]
275 You can, but probably shouldn't modify invoices...
277 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
278 supplied, replaces this record. If there is an error, returns the error,
279 otherwise returns false.
283 #replace can be inherited from Record.pm
285 # replace_check is now the preferred way to #implement replace data checks
286 # (so $object->replace() works without an argument)
289 my( $new, $old ) = ( shift, shift );
290 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
291 #return "Can't change _date!" unless $old->_date eq $new->_date;
292 return "Can't change _date" unless $old->_date == $new->_date;
293 return "Can't change charged" unless $old->charged == $new->charged
294 || $old->charged == 0
295 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
301 =item add_cc_surcharge
307 sub add_cc_surcharge {
308 my ($self, $pkgnum, $amount) = (shift, shift, shift);
311 my $cust_bill_pkg = new FS::cust_bill_pkg({
312 'invnum' => $self->invnum,
316 $error = $cust_bill_pkg->insert;
317 return $error if $error;
319 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
320 $self->charged($self->charged+$amount);
321 $error = $self->replace;
322 return $error if $error;
324 $self->apply_payments_and_credits;
330 Checks all fields to make sure this is a valid invoice. If there is an error,
331 returns the error, otherwise returns false. Called by the insert and replace
340 $self->ut_numbern('invnum')
341 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
342 || $self->ut_numbern('_date')
343 || $self->ut_money('charged')
344 || $self->ut_numbern('printed')
345 || $self->ut_enum('closed', [ '', 'Y' ])
346 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
347 || $self->ut_numbern('agent_invid') #varchar?
349 return $error if $error;
351 $self->_date(time) unless $self->_date;
353 $self->printed(0) if $self->printed eq '';
360 Returns the displayed invoice number for this invoice: agent_invid if
361 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
367 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
368 return $self->agent_invid;
370 return $self->invnum;
376 Returns a list consisting of the total previous balance for this customer,
377 followed by the previous outstanding invoices (as FS::cust_bill objects also).
384 my @cust_bill = sort { $a->_date <=> $b->_date }
385 grep { $_->owed != 0 && $_->_date < $self->_date }
386 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
388 foreach ( @cust_bill ) { $total += $_->owed; }
394 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
401 { 'table' => 'cust_bill_pkg',
402 'hashref' => { 'invnum' => $self->invnum },
403 'order_by' => 'ORDER BY billpkgnum',
408 =item cust_bill_pkg_pkgnum PKGNUM
410 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
415 sub cust_bill_pkg_pkgnum {
416 my( $self, $pkgnum ) = @_;
418 { 'table' => 'cust_bill_pkg',
419 'hashref' => { 'invnum' => $self->invnum,
422 'order_by' => 'ORDER BY billpkgnum',
429 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
436 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
437 $self->cust_bill_pkg;
439 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
444 Returns true if any of the packages (or their definitions) corresponding to the
445 line items for this invoice have the no_auto flag set.
451 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
454 =item open_cust_bill_pkg
456 Returns the open line items for this invoice.
458 Note that cust_bill_pkg with both setup and recur fees are returned as two
459 separate line items, each with only one fee.
463 # modeled after cust_main::open_cust_bill
464 sub open_cust_bill_pkg {
467 # grep { $_->owed > 0 } $self->cust_bill_pkg
469 my %other = ( 'recur' => 'setup',
470 'setup' => 'recur', );
472 foreach my $field ( qw( recur setup )) {
473 push @open, map { $_->set( $other{$field}, 0 ); $_; }
474 grep { $_->owed($field) > 0 }
475 $self->cust_bill_pkg;
481 =item cust_bill_event
483 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
487 sub cust_bill_event {
489 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
492 =item num_cust_bill_event
494 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
498 sub num_cust_bill_event {
501 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
502 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
503 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
504 $sth->fetchrow_arrayref->[0];
509 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
513 #false laziness w/cust_pkg.pm
517 'table' => 'cust_event',
518 'addl_from' => 'JOIN part_event USING ( eventpart )',
519 'hashref' => { 'tablenum' => $self->invnum },
520 'extra_sql' => " AND eventtable = 'cust_bill' ",
526 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
530 #false laziness w/cust_pkg.pm
534 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
535 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
536 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
537 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
538 $sth->fetchrow_arrayref->[0];
543 Returns the customer (see L<FS::cust_main>) for this invoice.
549 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
552 =item cust_suspend_if_balance_over AMOUNT
554 Suspends the customer associated with this invoice if the total amount owed on
555 this invoice and all older invoices is greater than the specified amount.
557 Returns a list: an empty list on success or a list of errors.
561 sub cust_suspend_if_balance_over {
562 my( $self, $amount ) = ( shift, shift );
563 my $cust_main = $self->cust_main;
564 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
567 $cust_main->suspend(@_);
573 Depreciated. See the cust_credited method.
575 #Returns a list consisting of the total previous credited (see
576 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
577 #outstanding credits (FS::cust_credit objects).
583 croak "FS::cust_bill->cust_credit depreciated; see ".
584 "FS::cust_bill->cust_credit_bill";
587 #my @cust_credit = sort { $a->_date <=> $b->_date }
588 # grep { $_->credited != 0 && $_->_date < $self->_date }
589 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
591 #foreach (@cust_credit) { $total += $_->credited; }
592 #$total, @cust_credit;
597 Depreciated. See the cust_bill_pay method.
599 #Returns all payments (see L<FS::cust_pay>) for this invoice.
605 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
607 #sort { $a->_date <=> $b->_date }
608 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
614 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
617 sub cust_bill_pay_batch {
619 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
624 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
630 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
631 sort { $a->_date <=> $b->_date }
632 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
637 =item cust_credit_bill
639 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
645 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
646 sort { $a->_date <=> $b->_date }
647 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
651 sub cust_credit_bill {
652 shift->cust_credited(@_);
655 #=item cust_bill_pay_pkgnum PKGNUM
657 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
658 #with matching pkgnum.
662 #sub cust_bill_pay_pkgnum {
663 # my( $self, $pkgnum ) = @_;
664 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
665 # sort { $a->_date <=> $b->_date }
666 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
667 # 'pkgnum' => $pkgnum,
672 =item cust_bill_pay_pkg PKGNUM
674 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
675 applied against the matching pkgnum.
679 sub cust_bill_pay_pkg {
680 my( $self, $pkgnum ) = @_;
683 'select' => 'cust_bill_pay_pkg.*',
684 'table' => 'cust_bill_pay_pkg',
685 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
686 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
687 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
688 " AND cust_bill_pkg.pkgnum = $pkgnum",
693 #=item cust_credited_pkgnum PKGNUM
695 #=item cust_credit_bill_pkgnum PKGNUM
697 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
698 #with matching pkgnum.
702 #sub cust_credited_pkgnum {
703 # my( $self, $pkgnum ) = @_;
704 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
705 # sort { $a->_date <=> $b->_date }
706 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
707 # 'pkgnum' => $pkgnum,
712 #sub cust_credit_bill_pkgnum {
713 # shift->cust_credited_pkgnum(@_);
716 =item cust_credit_bill_pkg PKGNUM
718 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
719 applied against the matching pkgnum.
723 sub cust_credit_bill_pkg {
724 my( $self, $pkgnum ) = @_;
727 'select' => 'cust_credit_bill_pkg.*',
728 'table' => 'cust_credit_bill_pkg',
729 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
730 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
731 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
732 " AND cust_bill_pkg.pkgnum = $pkgnum",
737 =item cust_bill_batch
739 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
743 sub cust_bill_batch {
745 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
750 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
757 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
759 foreach (@taxlines) { $total += $_->setup; }
765 Returns the amount owed (still outstanding) on this invoice, which is charged
766 minus all payment applications (see L<FS::cust_bill_pay>) and credit
767 applications (see L<FS::cust_credit_bill>).
773 my $balance = $self->charged;
774 $balance -= $_->amount foreach ( $self->cust_bill_pay );
775 $balance -= $_->amount foreach ( $self->cust_credited );
776 $balance = sprintf( "%.2f", $balance);
777 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
782 my( $self, $pkgnum ) = @_;
784 #my $balance = $self->charged;
786 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
788 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
789 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
791 $balance = sprintf( "%.2f", $balance);
792 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
796 =item apply_payments_and_credits [ OPTION => VALUE ... ]
798 Applies unapplied payments and credits to this invoice.
800 A hash of optional arguments may be passed. Currently "manual" is supported.
801 If true, a payment receipt is sent instead of a statement when
802 'payment_receipt_email' configuration option is set.
804 If there is an error, returns the error, otherwise returns false.
808 sub apply_payments_and_credits {
809 my( $self, %options ) = @_;
811 local $SIG{HUP} = 'IGNORE';
812 local $SIG{INT} = 'IGNORE';
813 local $SIG{QUIT} = 'IGNORE';
814 local $SIG{TERM} = 'IGNORE';
815 local $SIG{TSTP} = 'IGNORE';
816 local $SIG{PIPE} = 'IGNORE';
818 my $oldAutoCommit = $FS::UID::AutoCommit;
819 local $FS::UID::AutoCommit = 0;
822 $self->select_for_update; #mutex
824 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
825 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
827 if ( $conf->exists('pkg-balances') ) {
828 # limit @payments & @credits to those w/ a pkgnum grepped from $self
829 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
830 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
831 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
834 while ( $self->owed > 0 and ( @payments || @credits ) ) {
837 if ( @payments && @credits ) {
839 #decide which goes first by weight of top (unapplied) line item
841 my @open_lineitems = $self->open_cust_bill_pkg;
844 max( map { $_->part_pkg->pay_weight || 0 }
849 my $max_credit_weight =
850 max( map { $_->part_pkg->credit_weight || 0 }
856 #if both are the same... payments first? it has to be something
857 if ( $max_pay_weight >= $max_credit_weight ) {
863 } elsif ( @payments ) {
865 } elsif ( @credits ) {
868 die "guru meditation #12 and 35";
872 if ( $app eq 'pay' ) {
874 my $payment = shift @payments;
875 $unapp_amount = $payment->unapplied;
876 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
877 $app->pkgnum( $payment->pkgnum )
878 if $conf->exists('pkg-balances') && $payment->pkgnum;
880 } elsif ( $app eq 'credit' ) {
882 my $credit = shift @credits;
883 $unapp_amount = $credit->credited;
884 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
885 $app->pkgnum( $credit->pkgnum )
886 if $conf->exists('pkg-balances') && $credit->pkgnum;
889 die "guru meditation #12 and 35";
893 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
894 warn "owed_pkgnum ". $app->pkgnum;
895 $owed = $self->owed_pkgnum($app->pkgnum);
899 next unless $owed > 0;
901 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
902 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
904 $app->invnum( $self->invnum );
906 my $error = $app->insert(%options);
908 $dbh->rollback if $oldAutoCommit;
909 return "Error inserting ". $app->table. " record: $error";
911 die $error if $error;
915 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
920 =item generate_email OPTION => VALUE ...
928 sender address, required
932 alternate template name, optional
936 text attachment arrayref, optional
940 email subject, optional
944 notice name instead of "Invoice", optional
948 Returns an argument list to be passed to L<FS::Misc::send_email>.
959 my $me = '[FS::cust_bill::generate_email]';
962 'from' => $args{'from'},
963 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
967 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
968 'template' => $args{'template'},
969 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
970 'no_coupon' => $args{'no_coupon'},
973 my $cust_main = $self->cust_main;
975 if (ref($args{'to'}) eq 'ARRAY') {
976 $return{'to'} = $args{'to'};
978 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
979 $cust_main->invoicing_list
983 if ( $conf->exists('invoice_html') ) {
985 warn "$me creating HTML/text multipart message"
988 $return{'nobody'} = 1;
990 my $alternative = build MIME::Entity
991 'Type' => 'multipart/alternative',
992 'Encoding' => '7bit',
993 'Disposition' => 'inline'
997 if ( $conf->exists('invoice_email_pdf')
998 and scalar($conf->config('invoice_email_pdf_note')) ) {
1000 warn "$me using 'invoice_email_pdf_note' in multipart message"
1002 $data = [ map { $_ . "\n" }
1003 $conf->config('invoice_email_pdf_note')
1008 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1010 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1011 $data = $args{'print_text'};
1013 $data = [ $self->print_text(\%opt) ];
1018 $alternative->attach(
1019 'Type' => 'text/plain',
1020 #'Encoding' => 'quoted-printable',
1021 'Encoding' => '7bit',
1023 'Disposition' => 'inline',
1026 $args{'from'} =~ /\@([\w\.\-]+)/;
1027 my $from = $1 || 'example.com';
1028 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1031 my $agentnum = $cust_main->agentnum;
1032 if ( defined($args{'template'}) && length($args{'template'})
1033 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1036 $logo = 'logo_'. $args{'template'}. '.png';
1040 my $image_data = $conf->config_binary( $logo, $agentnum);
1042 my $image = build MIME::Entity
1043 'Type' => 'image/png',
1044 'Encoding' => 'base64',
1045 'Data' => $image_data,
1046 'Filename' => 'logo.png',
1047 'Content-ID' => "<$content_id>",
1051 if($conf->exists('invoice-barcode')){
1052 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1053 $barcode = build MIME::Entity
1054 'Type' => 'image/png',
1055 'Encoding' => 'base64',
1056 'Data' => $self->invoice_barcode(0),
1057 'Filename' => 'barcode.png',
1058 'Content-ID' => "<$barcode_content_id>",
1060 $opt{'barcode_cid'} = $barcode_content_id;
1063 $alternative->attach(
1064 'Type' => 'text/html',
1065 'Encoding' => 'quoted-printable',
1066 'Data' => [ '<html>',
1069 ' '. encode_entities($return{'subject'}),
1072 ' <body bgcolor="#e8e8e8">',
1073 $self->print_html({ 'cid'=>$content_id, %opt }),
1077 'Disposition' => 'inline',
1078 #'Filename' => 'invoice.pdf',
1081 my @otherparts = ();
1082 if ( $cust_main->email_csv_cdr ) {
1084 push @otherparts, build MIME::Entity
1085 'Type' => 'text/csv',
1086 'Encoding' => '7bit',
1087 'Data' => [ map { "$_\n" }
1088 $self->call_details('prepend_billed_number' => 1)
1090 'Disposition' => 'attachment',
1091 'Filename' => 'usage-'. $self->invnum. '.csv',
1096 if ( $conf->exists('invoice_email_pdf') ) {
1101 # multipart/alternative
1107 my $related = build MIME::Entity 'Type' => 'multipart/related',
1108 'Encoding' => '7bit';
1110 #false laziness w/Misc::send_email
1111 $related->head->replace('Content-type',
1112 $related->mime_type.
1113 '; boundary="'. $related->head->multipart_boundary. '"'.
1114 '; type=multipart/alternative'
1117 $related->add_part($alternative);
1119 $related->add_part($image);
1121 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1123 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1127 #no other attachment:
1129 # multipart/alternative
1134 $return{'content-type'} = 'multipart/related';
1135 if($conf->exists('invoice-barcode')){
1136 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1139 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1141 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1142 #$return{'disposition'} = 'inline';
1148 if ( $conf->exists('invoice_email_pdf') ) {
1149 warn "$me creating PDF attachment"
1152 #mime parts arguments a la MIME::Entity->build().
1153 $return{'mimeparts'} = [
1154 { $self->mimebuild_pdf(\%opt) }
1158 if ( $conf->exists('invoice_email_pdf')
1159 and scalar($conf->config('invoice_email_pdf_note')) ) {
1161 warn "$me using 'invoice_email_pdf_note'"
1163 $return{'body'} = [ map { $_ . "\n" }
1164 $conf->config('invoice_email_pdf_note')
1169 warn "$me not using 'invoice_email_pdf_note'"
1171 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1172 $return{'body'} = $args{'print_text'};
1174 $return{'body'} = [ $self->print_text(\%opt) ];
1187 Returns a list suitable for passing to MIME::Entity->build(), representing
1188 this invoice as PDF attachment.
1195 'Type' => 'application/pdf',
1196 'Encoding' => 'base64',
1197 'Data' => [ $self->print_pdf(@_) ],
1198 'Disposition' => 'attachment',
1199 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1203 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1205 Sends this invoice to the destinations configured for this customer: sends
1206 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1208 Options can be passed as a hashref (recommended) or as a list of up to
1209 four values for templatename, agentnum, invoice_from and amount.
1211 I<template>, if specified, is the name of a suffix for alternate invoices.
1213 I<agentnum>, if specified, means that this invoice will only be sent for customers
1214 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1215 single agent) or an arrayref of agentnums.
1217 I<invoice_from>, if specified, overrides the default email invoice From: address.
1219 I<amount>, if specified, only sends the invoice if the total amount owed on this
1220 invoice and all older invoices is greater than the specified amount.
1222 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1226 sub queueable_send {
1229 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1230 or die "invalid invoice number: " . $opt{invnum};
1232 my @args = ( $opt{template}, $opt{agentnum} );
1233 push @args, $opt{invoice_from}
1234 if exists($opt{invoice_from}) && $opt{invoice_from};
1236 my $error = $self->send( @args );
1237 die $error if $error;
1244 my( $template, $invoice_from, $notice_name );
1246 my $balance_over = 0;
1250 $template = $opt->{'template'} || '';
1251 if ( $agentnums = $opt->{'agentnum'} ) {
1252 $agentnums = [ $agentnums ] unless ref($agentnums);
1254 $invoice_from = $opt->{'invoice_from'};
1255 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1256 $notice_name = $opt->{'notice_name'};
1258 $template = scalar(@_) ? shift : '';
1259 if ( scalar(@_) && $_[0] ) {
1260 $agentnums = ref($_[0]) ? shift : [ shift ];
1262 $invoice_from = shift if scalar(@_);
1263 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1266 return 'N/A' unless ! $agentnums
1267 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1270 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1272 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1273 $conf->config('invoice_from', $self->cust_main->agentnum );
1276 'template' => $template,
1277 'invoice_from' => $invoice_from,
1278 'notice_name' => ( $notice_name || 'Invoice' ),
1281 my @invoicing_list = $self->cust_main->invoicing_list;
1283 #$self->email_invoice(\%opt)
1285 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1287 #$self->print_invoice(\%opt)
1289 if grep { $_ eq 'POST' } @invoicing_list; #postal
1291 $self->fax_invoice(\%opt)
1292 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1298 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1300 Emails this invoice.
1302 Options can be passed as a hashref (recommended) or as a list of up to
1303 two values for templatename and invoice_from.
1305 I<template>, if specified, is the name of a suffix for alternate invoices.
1307 I<invoice_from>, if specified, overrides the default email invoice From: address.
1309 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1313 sub queueable_email {
1316 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1317 or die "invalid invoice number: " . $opt{invnum};
1319 my %args = ( 'template' => $opt{template} );
1320 $args{$_} = $opt{$_}
1321 foreach grep { exists($opt{$_}) && $opt{$_} }
1322 qw( invoice_from notice_name no_coupon );
1324 my $error = $self->email( \%args );
1325 die $error if $error;
1329 #sub email_invoice {
1333 my( $template, $invoice_from, $notice_name, $no_coupon );
1336 $template = $opt->{'template'} || '';
1337 $invoice_from = $opt->{'invoice_from'};
1338 $notice_name = $opt->{'notice_name'} || 'Invoice';
1339 $no_coupon = $opt->{'no_coupon'} || 0;
1341 $template = scalar(@_) ? shift : '';
1342 $invoice_from = shift if scalar(@_);
1343 $notice_name = 'Invoice';
1347 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1348 $conf->config('invoice_from', $self->cust_main->agentnum );
1350 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1351 $self->cust_main->invoicing_list;
1353 if ( ! @invoicing_list ) { #no recipients
1354 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1355 die 'No recipients for customer #'. $self->custnum;
1357 #default: better to notify this person than silence
1358 @invoicing_list = ($invoice_from);
1362 my $subject = $self->email_subject($template);
1364 my $error = send_email(
1365 $self->generate_email(
1366 'from' => $invoice_from,
1367 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1368 'subject' => $subject,
1369 'template' => $template,
1370 'notice_name' => $notice_name,
1371 'no_coupon' => $no_coupon,
1374 die "can't email invoice: $error\n" if $error;
1375 #die "$error\n" if $error;
1382 #my $template = scalar(@_) ? shift : '';
1385 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1388 my $cust_main = $self->cust_main;
1389 my $name = $cust_main->name;
1390 my $name_short = $cust_main->name_short;
1391 my $invoice_number = $self->invnum;
1392 my $invoice_date = $self->_date_pretty;
1394 eval qq("$subject");
1397 =item lpr_data HASHREF | [ TEMPLATE ]
1399 Returns the postscript or plaintext for this invoice as an arrayref.
1401 Options can be passed as a hashref (recommended) or as a single optional value
1404 I<template>, if specified, is the name of a suffix for alternate invoices.
1406 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1412 my( $template, $notice_name );
1415 $template = $opt->{'template'} || '';
1416 $notice_name = $opt->{'notice_name'} || 'Invoice';
1418 $template = scalar(@_) ? shift : '';
1419 $notice_name = 'Invoice';
1423 'template' => $template,
1424 'notice_name' => $notice_name,
1427 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1428 [ $self->$method( \%opt ) ];
1431 =item print HASHREF | [ TEMPLATE ]
1433 Prints this invoice.
1435 Options can be passed as a hashref (recommended) or as a single optional
1438 I<template>, if specified, is the name of a suffix for alternate invoices.
1440 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1444 #sub print_invoice {
1447 my( $template, $notice_name );
1450 $template = $opt->{'template'} || '';
1451 $notice_name = $opt->{'notice_name'} || 'Invoice';
1453 $template = scalar(@_) ? shift : '';
1454 $notice_name = 'Invoice';
1458 'template' => $template,
1459 'notice_name' => $notice_name,
1462 if($conf->exists('invoice_print_pdf')) {
1463 # Add the invoice to the current batch.
1464 $self->batch_invoice(\%opt);
1467 do_print $self->lpr_data(\%opt);
1471 =item fax_invoice HASHREF | [ TEMPLATE ]
1475 Options can be passed as a hashref (recommended) or as a single optional
1478 I<template>, if specified, is the name of a suffix for alternate invoices.
1480 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1486 my( $template, $notice_name );
1489 $template = $opt->{'template'} || '';
1490 $notice_name = $opt->{'notice_name'} || 'Invoice';
1492 $template = scalar(@_) ? shift : '';
1493 $notice_name = 'Invoice';
1496 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1497 unless $conf->exists('invoice_latex');
1499 my $dialstring = $self->cust_main->getfield('fax');
1503 'template' => $template,
1504 'notice_name' => $notice_name,
1507 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1508 'dialstring' => $dialstring,
1510 die $error if $error;
1514 =item batch_invoice [ HASHREF ]
1516 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1517 isn't an open batch, one will be created.
1522 my ($self, $opt) = @_;
1523 my $batch = FS::bill_batch->get_open_batch;
1524 my $cust_bill_batch = FS::cust_bill_batch->new({
1525 batchnum => $batch->batchnum,
1526 invnum => $self->invnum,
1528 return $cust_bill_batch->insert($opt);
1531 =item ftp_invoice [ TEMPLATENAME ]
1533 Sends this invoice data via FTP.
1535 TEMPLATENAME is unused?
1541 my $template = scalar(@_) ? shift : '';
1544 'protocol' => 'ftp',
1545 'server' => $conf->config('cust_bill-ftpserver'),
1546 'username' => $conf->config('cust_bill-ftpusername'),
1547 'password' => $conf->config('cust_bill-ftppassword'),
1548 'dir' => $conf->config('cust_bill-ftpdir'),
1549 'format' => $conf->config('cust_bill-ftpformat'),
1553 =item spool_invoice [ TEMPLATENAME ]
1555 Spools this invoice data (see L<FS::spool_csv>)
1557 TEMPLATENAME is unused?
1563 my $template = scalar(@_) ? shift : '';
1566 'format' => $conf->config('cust_bill-spoolformat'),
1567 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1571 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1573 Like B<send>, but only sends the invoice if it is the newest open invoice for
1578 sub send_if_newest {
1583 grep { $_->owed > 0 }
1584 qsearch('cust_bill', {
1585 'custnum' => $self->custnum,
1586 #'_date' => { op=>'>', value=>$self->_date },
1587 'invnum' => { op=>'>', value=>$self->invnum },
1594 =item send_csv OPTION => VALUE, ...
1596 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1600 protocol - currently only "ftp"
1606 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1607 and YYMMDDHHMMSS is a timestamp.
1609 See L</print_csv> for a description of the output format.
1614 my($self, %opt) = @_;
1618 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1619 mkdir $spooldir, 0700 unless -d $spooldir;
1621 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1622 my $file = "$spooldir/$tracctnum.csv";
1624 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1626 open(CSV, ">$file") or die "can't open $file: $!";
1634 if ( $opt{protocol} eq 'ftp' ) {
1635 eval "use Net::FTP;";
1637 $net = Net::FTP->new($opt{server}) or die @$;
1639 die "unknown protocol: $opt{protocol}";
1642 $net->login( $opt{username}, $opt{password} )
1643 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1645 $net->binary or die "can't set binary mode";
1647 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1649 $net->put($file) or die "can't put $file: $!";
1659 Spools CSV invoice data.
1665 =item format - 'default' or 'billco'
1667 =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>).
1669 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1671 =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.
1678 my($self, %opt) = @_;
1680 my $cust_main = $self->cust_main;
1682 if ( $opt{'dest'} ) {
1683 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1684 $cust_main->invoicing_list;
1685 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1686 || ! keys %invoicing_list;
1689 if ( $opt{'balanceover'} ) {
1691 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1694 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1695 mkdir $spooldir, 0700 unless -d $spooldir;
1697 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1701 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1702 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1705 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1707 open(CSV, ">>$file") or die "can't open $file: $!";
1708 flock(CSV, LOCK_EX);
1713 if ( lc($opt{'format'}) eq 'billco' ) {
1715 flock(CSV, LOCK_UN);
1720 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1723 open(CSV,">>$file") or die "can't open $file: $!";
1724 flock(CSV, LOCK_EX);
1730 flock(CSV, LOCK_UN);
1737 =item print_csv OPTION => VALUE, ...
1739 Returns CSV data for this invoice.
1743 format - 'default' or 'billco'
1745 Returns a list consisting of two scalars. The first is a single line of CSV
1746 header information for this invoice. The second is one or more lines of CSV
1747 detail information for this invoice.
1749 If I<format> is not specified or "default", the fields of the CSV file are as
1752 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1756 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1758 B<record_type> is C<cust_bill> for the initial header line only. The
1759 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1760 fields are filled in.
1762 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1763 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1766 =item invnum - invoice number
1768 =item custnum - customer number
1770 =item _date - invoice date
1772 =item charged - total invoice amount
1774 =item first - customer first name
1776 =item last - customer first name
1778 =item company - company name
1780 =item address1 - address line 1
1782 =item address2 - address line 1
1792 =item pkg - line item description
1794 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1796 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1798 =item sdate - start date for recurring fee
1800 =item edate - end date for recurring fee
1804 If I<format> is "billco", the fields of the header CSV file are as follows:
1806 +-------------------------------------------------------------------+
1807 | FORMAT HEADER FILE |
1808 |-------------------------------------------------------------------|
1809 | Field | Description | Name | Type | Width |
1810 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1811 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1812 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1813 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1814 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1815 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1816 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1817 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1818 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1819 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1820 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1821 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1822 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1823 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1824 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1825 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1826 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1827 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1828 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1829 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1830 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1831 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1832 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1833 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1834 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1835 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1836 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1837 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1838 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1839 +-------+-------------------------------+------------+------+-------+
1841 If I<format> is "billco", the fields of the detail CSV file are as follows:
1843 FORMAT FOR DETAIL FILE
1845 Field | Description | Name | Type | Width
1846 1 | N/A-Leave Empty | RC | CHAR | 2
1847 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1848 3 | Account Number | TRACCTNUM | CHAR | 15
1849 4 | Invoice Number | TRINVOICE | CHAR | 15
1850 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1851 6 | Transaction Detail | DETAILS | CHAR | 100
1852 7 | Amount | AMT | NUM* | 9
1853 8 | Line Format Control** | LNCTRL | CHAR | 2
1854 9 | Grouping Code | GROUP | CHAR | 2
1855 10 | User Defined | ACCT CODE | CHAR | 15
1860 my($self, %opt) = @_;
1862 eval "use Text::CSV_XS";
1865 my $cust_main = $self->cust_main;
1867 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1869 if ( lc($opt{'format'}) eq 'billco' ) {
1872 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1874 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1876 my( $previous_balance, @unused ) = $self->previous; #previous balance
1878 my $pmt_cr_applied = 0;
1879 $pmt_cr_applied += $_->{'amount'}
1880 foreach ( $self->_items_payments, $self->_items_credits ) ;
1882 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1885 '', # 1 | N/A-Leave Empty CHAR 2
1886 '', # 2 | N/A-Leave Empty CHAR 15
1887 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1888 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1889 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1890 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1891 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1892 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1893 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1894 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1895 '', # 10 | Ancillary Billing Information CHAR 30
1896 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1897 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1900 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1903 $duedate, # 14 | Bill Due Date CHAR 10
1905 $previous_balance, # 15 | Previous Balance NUM* 9
1906 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1907 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1908 $totaldue, # 18 | Total Amt Due NUM* 9
1909 $totaldue, # 19 | Total Amt Due NUM* 9
1910 '', # 20 | 30 Day Aging NUM* 9
1911 '', # 21 | 60 Day Aging NUM* 9
1912 '', # 22 | 90 Day Aging NUM* 9
1913 'N', # 23 | Y/N CHAR 1
1914 '', # 24 | Remittance automation CHAR 100
1915 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1916 $self->custnum, # 26 | Customer Reference Number CHAR 15
1917 '0', # 27 | Federal Tax*** NUM* 9
1918 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1919 '0', # 29 | Other Taxes & Fees*** NUM* 9
1928 time2str("%x", $self->_date),
1929 sprintf("%.2f", $self->charged),
1930 ( map { $cust_main->getfield($_) }
1931 qw( first last company address1 address2 city state zip country ) ),
1933 ) or die "can't create csv";
1936 my $header = $csv->string. "\n";
1939 if ( lc($opt{'format'}) eq 'billco' ) {
1942 foreach my $item ( $self->_items_pkg ) {
1945 '', # 1 | N/A-Leave Empty CHAR 2
1946 '', # 2 | N/A-Leave Empty CHAR 15
1947 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1948 $self->invnum, # 4 | Invoice Number CHAR 15
1949 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1950 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1951 $item->{'amount'}, # 7 | Amount NUM* 9
1952 '', # 8 | Line Format Control** CHAR 2
1953 '', # 9 | Grouping Code CHAR 2
1954 '', # 10 | User Defined CHAR 15
1957 $detail .= $csv->string. "\n";
1963 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1965 my($pkg, $setup, $recur, $sdate, $edate);
1966 if ( $cust_bill_pkg->pkgnum ) {
1968 ($pkg, $setup, $recur, $sdate, $edate) = (
1969 $cust_bill_pkg->part_pkg->pkg,
1970 ( $cust_bill_pkg->setup != 0
1971 ? sprintf("%.2f", $cust_bill_pkg->setup )
1973 ( $cust_bill_pkg->recur != 0
1974 ? sprintf("%.2f", $cust_bill_pkg->recur )
1976 ( $cust_bill_pkg->sdate
1977 ? time2str("%x", $cust_bill_pkg->sdate)
1979 ($cust_bill_pkg->edate
1980 ?time2str("%x", $cust_bill_pkg->edate)
1984 } else { #pkgnum tax
1985 next unless $cust_bill_pkg->setup != 0;
1986 $pkg = $cust_bill_pkg->desc;
1987 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1988 ( $sdate, $edate ) = ( '', '' );
1994 ( map { '' } (1..11) ),
1995 ($pkg, $setup, $recur, $sdate, $edate)
1996 ) or die "can't create csv";
1998 $detail .= $csv->string. "\n";
2004 ( $header, $detail );
2010 Pays this invoice with a compliemntary payment. If there is an error,
2011 returns the error, otherwise returns false.
2017 my $cust_pay = new FS::cust_pay ( {
2018 'invnum' => $self->invnum,
2019 'paid' => $self->owed,
2022 'payinfo' => $self->cust_main->payinfo,
2030 Attempts to pay this invoice with a credit card payment via a
2031 Business::OnlinePayment realtime gateway. See
2032 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2033 for supported processors.
2039 $self->realtime_bop( 'CC', @_ );
2044 Attempts to pay this invoice with an electronic check (ACH) payment via a
2045 Business::OnlinePayment realtime gateway. See
2046 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2047 for supported processors.
2053 $self->realtime_bop( 'ECHECK', @_ );
2058 Attempts to pay this invoice with phone bill (LEC) payment via a
2059 Business::OnlinePayment realtime gateway. See
2060 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2061 for supported processors.
2067 $self->realtime_bop( 'LEC', @_ );
2071 my( $self, $method ) = (shift,shift);
2074 my $cust_main = $self->cust_main;
2075 my $balance = $cust_main->balance;
2076 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2077 $amount = sprintf("%.2f", $amount);
2078 return "not run (balance $balance)" unless $amount > 0;
2080 my $description = 'Internet Services';
2081 if ( $conf->exists('business-onlinepayment-description') ) {
2082 my $dtempl = $conf->config('business-onlinepayment-description');
2084 my $agent_obj = $cust_main->agent
2085 or die "can't retreive agent for $cust_main (agentnum ".
2086 $cust_main->agentnum. ")";
2087 my $agent = $agent_obj->agent;
2088 my $pkgs = join(', ',
2089 map { $_->part_pkg->pkg }
2090 grep { $_->pkgnum } $self->cust_bill_pkg
2092 $description = eval qq("$dtempl");
2095 $cust_main->realtime_bop($method, $amount,
2096 'description' => $description,
2097 'invnum' => $self->invnum,
2098 #this didn't do what we want, it just calls apply_payments_and_credits
2100 'apply_to_invoice' => 1,
2103 #this changes application behavior: auto payments
2104 #triggered against a specific invoice are now applied
2105 #to that invoice instead of oldest open.
2111 =item batch_card OPTION => VALUE...
2113 Adds a payment for this invoice to the pending credit card batch (see
2114 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2115 runs the payment using a realtime gateway.
2120 my ($self, %options) = @_;
2121 my $cust_main = $self->cust_main;
2123 $options{invnum} = $self->invnum;
2125 $cust_main->batch_card(%options);
2128 sub _agent_template {
2130 $self->cust_main->agent_template;
2133 sub _agent_invoice_from {
2135 $self->cust_main->agent_invoice_from;
2138 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2140 Returns an text invoice, as a list of lines.
2142 Options can be passed as a hashref (recommended) or as a list of time, template
2143 and then any key/value pairs for any other options.
2145 I<time>, if specified, is used to control the printing of overdue messages. The
2146 default is now. It isn't the date of the invoice; that's the `_date' field.
2147 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2148 L<Time::Local> and L<Date::Parse> for conversion functions.
2150 I<template>, if specified, is the name of a suffix for alternate invoices.
2152 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2158 my( $today, $template, %opt );
2160 %opt = %{ shift() };
2161 $today = delete($opt{'time'}) || '';
2162 $template = delete($opt{template}) || '';
2164 ( $today, $template, %opt ) = @_;
2167 my %params = ( 'format' => 'template' );
2168 $params{'time'} = $today if $today;
2169 $params{'template'} = $template if $template;
2170 $params{$_} = $opt{$_}
2171 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2173 $self->print_generic( %params );
2176 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2178 Internal method - returns a filename of a filled-in LaTeX template for this
2179 invoice (Note: add ".tex" to get the actual filename), and a filename of
2180 an associated logo (with the .eps extension included).
2182 See print_ps and print_pdf for methods that return PostScript and PDF output.
2184 Options can be passed as a hashref (recommended) or as a list of time, template
2185 and then any key/value pairs for any other options.
2187 I<time>, if specified, is used to control the printing of overdue messages. The
2188 default is now. It isn't the date of the invoice; that's the `_date' field.
2189 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2190 L<Time::Local> and L<Date::Parse> for conversion functions.
2192 I<template>, if specified, is the name of a suffix for alternate invoices.
2194 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2200 my( $today, $template, %opt );
2202 %opt = %{ shift() };
2203 $today = delete($opt{'time'}) || '';
2204 $template = delete($opt{template}) || '';
2206 ( $today, $template, %opt ) = @_;
2209 my %params = ( 'format' => 'latex' );
2210 $params{'time'} = $today if $today;
2211 $params{'template'} = $template if $template;
2212 $params{$_} = $opt{$_}
2213 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2215 $template ||= $self->_agent_template;
2217 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2218 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2222 ) or die "can't open temp file: $!\n";
2224 my $agentnum = $self->cust_main->agentnum;
2226 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2227 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2228 or die "can't write temp file: $!\n";
2230 print $lh $conf->config_binary('logo.eps', $agentnum)
2231 or die "can't write temp file: $!\n";
2234 $params{'logo_file'} = $lh->filename;
2236 if($conf->exists('invoice-barcode')){
2237 my $png_file = $self->invoice_barcode($dir);
2238 my $eps_file = $png_file;
2239 $eps_file =~ s/\.png$/.eps/g;
2240 $png_file =~ /(barcode.*png)/;
2242 $eps_file =~ /(barcode.*eps)/;
2245 my $curr_dir = cwd();
2247 # after painfuly long experimentation, it was determined that sam2p won't
2248 # accept : and other chars in the path, no matter how hard I tried to
2249 # escape them, hence the chdir (and chdir back, just to be safe)
2250 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2251 or die "sam2p failed: $!\n";
2255 $params{'barcode_file'} = $eps_file;
2258 my @filled_in = $self->print_generic( %params );
2260 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2264 ) or die "can't open temp file: $!\n";
2265 print $fh join('', @filled_in );
2268 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2269 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2273 =item invoice_barcode DIR_OR_FALSE
2275 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2276 it is taken as the temp directory where the PNG file will be generated and the
2277 PNG file name is returned. Otherwise, the PNG image itself is returned.
2281 sub invoice_barcode {
2282 my ($self, $dir) = (shift,shift);
2284 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2285 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2286 my $gd = $gdbar->plot(Height => 30);
2289 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2293 ) or die "can't open temp file: $!\n";
2294 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2295 my $png_file = $bh->filename;
2302 =item print_generic OPTION => VALUE ...
2304 Internal method - returns a filled-in template for this invoice as a scalar.
2306 See print_ps and print_pdf for methods that return PostScript and PDF output.
2308 Non optional options include
2309 format - latex, html, template
2311 Optional options include
2313 template - a value used as a suffix for a configuration template
2315 time - a value used to control the printing of overdue messages. The
2316 default is now. It isn't the date of the invoice; that's the `_date' field.
2317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2318 L<Time::Local> and L<Date::Parse> for conversion functions.
2322 unsquelch_cdr - overrides any per customer cdr squelching when true
2324 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2328 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2329 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2330 # yes: fixed width (dot matrix) text printing will be borked
2333 my( $self, %params ) = @_;
2334 my $today = $params{today} ? $params{today} : time;
2335 warn "$me print_generic called on $self with suffix $params{template}\n"
2338 my $format = $params{format};
2339 die "Unknown format: $format"
2340 unless $format =~ /^(latex|html|template)$/;
2342 my $cust_main = $self->cust_main;
2343 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2344 unless $cust_main->payname
2345 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2347 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2348 'html' => [ '<%=', '%>' ],
2349 'template' => [ '{', '}' ],
2352 warn "$me print_generic creating template\n"
2355 #create the template
2356 my $template = $params{template} ? $params{template} : $self->_agent_template;
2357 my $templatefile = "invoice_$format";
2358 $templatefile .= "_$template"
2359 if length($template) && $conf->exists($templatefile."_$template");
2360 my @invoice_template = map "$_\n", $conf->config($templatefile)
2361 or die "cannot load config data $templatefile";
2364 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2365 #change this to a die when the old code is removed
2366 warn "old-style invoice template $templatefile; ".
2367 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2368 $old_latex = 'true';
2369 @invoice_template = _translate_old_latex_format(@invoice_template);
2372 warn "$me print_generic creating T:T object\n"
2375 my $text_template = new Text::Template(
2377 SOURCE => \@invoice_template,
2378 DELIMITERS => $delimiters{$format},
2381 warn "$me print_generic compiling T:T object\n"
2384 $text_template->compile()
2385 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2388 # additional substitution could possibly cause breakage in existing templates
2389 my %convert_maps = (
2391 'notes' => sub { map "$_", @_ },
2392 'footer' => sub { map "$_", @_ },
2393 'smallfooter' => sub { map "$_", @_ },
2394 'returnaddress' => sub { map "$_", @_ },
2395 'coupon' => sub { map "$_", @_ },
2396 'summary' => sub { map "$_", @_ },
2402 s/%%(.*)$/<!-- $1 -->/g;
2403 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2404 s/\\begin\{enumerate\}/<ol>/g;
2406 s/\\end\{enumerate\}/<\/ol>/g;
2407 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2416 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2418 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2423 s/\\\\\*?\s*$/<BR>/;
2424 s/\\hyphenation\{[\w\s\-]+}//;
2429 'coupon' => sub { "" },
2430 'summary' => sub { "" },
2437 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2438 s/\\begin\{enumerate\}//g;
2440 s/\\end\{enumerate\}//g;
2441 s/\\textbf\{(.*)\}/$1/g;
2448 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2450 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2455 s/\\\\\*?\s*$/\n/; # dubious
2456 s/\\hyphenation\{[\w\s\-]+}//;
2460 'coupon' => sub { "" },
2461 'summary' => sub { "" },
2466 # hashes for differing output formats
2467 my %nbsps = ( 'latex' => '~',
2468 'html' => '', # '&nbps;' would be nice
2469 'template' => '', # not used
2471 my $nbsp = $nbsps{$format};
2473 my %escape_functions = ( 'latex' => \&_latex_escape,
2474 'html' => \&_html_escape_nbsp,#\&encode_entities,
2475 'template' => sub { shift },
2477 my $escape_function = $escape_functions{$format};
2478 my $escape_function_nonbsp = ($format eq 'html')
2479 ? \&_html_escape : $escape_function;
2481 my %date_formats = ( 'latex' => $date_format_long,
2482 'html' => $date_format_long,
2485 $date_formats{'html'} =~ s/ / /g;
2487 my $date_format = $date_formats{$format};
2489 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2491 'html' => sub { return '<b>'. shift(). '</b>'
2493 'template' => sub { shift },
2495 my $embolden_function = $embolden_functions{$format};
2497 my %newline_tokens = ( 'latex' => '\\\\',
2501 my $newline_token = $newline_tokens{$format};
2503 warn "$me generating template variables\n"
2506 # generate template variables
2509 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2513 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2519 $returnaddress = join("\n",
2520 $conf->config_orbase("invoice_${format}returnaddress", $template)
2523 } elsif ( grep /\S/,
2524 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2526 my $convert_map = $convert_maps{$format}{'returnaddress'};
2529 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2534 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2536 my $convert_map = $convert_maps{$format}{'returnaddress'};
2537 $returnaddress = join( "\n", &$convert_map(
2538 map { s/( {2,})/'~' x length($1)/eg;
2542 ( $conf->config('company_name', $self->cust_main->agentnum),
2543 $conf->config('company_address', $self->cust_main->agentnum),
2550 my $warning = "Couldn't find a return address; ".
2551 "do you need to set the company_address configuration value?";
2553 $returnaddress = $nbsp;
2554 #$returnaddress = $warning;
2558 warn "$me generating invoice data\n"
2561 my $agentnum = $self->cust_main->agentnum;
2563 my %invoice_data = (
2566 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2567 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2568 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2569 'returnaddress' => $returnaddress,
2570 'agent' => &$escape_function($cust_main->agent->agent),
2573 'invnum' => $self->invnum,
2574 'date' => time2str($date_format, $self->_date),
2575 'today' => time2str($date_format_long, $today),
2576 'terms' => $self->terms,
2577 'template' => $template, #params{'template'},
2578 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2579 'current_charges' => sprintf("%.2f", $self->charged),
2580 'duedate' => $self->due_date2str($rdate_format), #date_format?
2583 'custnum' => $cust_main->display_custnum,
2584 'agent_custid' => &$escape_function($cust_main->agent_custid),
2585 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2586 payname company address1 address2 city state zip fax
2590 'ship_enable' => $conf->exists('invoice-ship_address'),
2591 'unitprices' => $conf->exists('invoice-unitprice'),
2592 'smallernotes' => $conf->exists('invoice-smallernotes'),
2593 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2594 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2596 #layout info -- would be fancy to calc some of this and bury the template
2598 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2599 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2600 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2601 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2602 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2603 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2604 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2605 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2606 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2607 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2609 # better hang on to conf_dir for a while (for old templates)
2610 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2612 #these are only used when doing paged plaintext
2618 my $min_sdate = 999999999999;
2620 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2621 next unless $cust_bill_pkg->pkgnum > 0;
2622 $min_sdate = $cust_bill_pkg->sdate
2623 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2624 $max_edate = $cust_bill_pkg->edate
2625 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2628 $invoice_data{'bill_period'} = '';
2629 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2630 . " to " . time2str('%e %h', $max_edate)
2631 if ($max_edate != 0 && $min_sdate != 999999999999);
2633 $invoice_data{finance_section} = '';
2634 if ( $conf->config('finance_pkgclass') ) {
2636 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2637 $invoice_data{finance_section} = $pkg_class->categoryname;
2639 $invoice_data{finance_amount} = '0.00';
2640 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2642 my $countrydefault = $conf->config('countrydefault') || 'US';
2643 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2644 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2645 my $method = $prefix.$_;
2646 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2648 $invoice_data{'ship_country'} = ''
2649 if ( $invoice_data{'ship_country'} eq $countrydefault );
2651 $invoice_data{'cid'} = $params{'cid'}
2654 if ( $cust_main->country eq $countrydefault ) {
2655 $invoice_data{'country'} = '';
2657 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2661 $invoice_data{'address'} = \@address;
2663 $cust_main->payname.
2664 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2665 ? " (P.O. #". $cust_main->payinfo. ")"
2669 push @address, $cust_main->company
2670 if $cust_main->company;
2671 push @address, $cust_main->address1;
2672 push @address, $cust_main->address2
2673 if $cust_main->address2;
2675 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2676 push @address, $invoice_data{'country'}
2677 if $invoice_data{'country'};
2679 while (scalar(@address) < 5);
2681 $invoice_data{'logo_file'} = $params{'logo_file'}
2682 if $params{'logo_file'};
2683 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2684 if $params{'barcode_file'};
2685 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2686 if $params{'barcode_img'};
2687 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2688 if $params{'barcode_cid'};
2690 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2691 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2692 #my $balance_due = $self->owed + $pr_total - $cr_total;
2693 my $balance_due = $self->owed + $pr_total;
2694 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2695 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2696 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2697 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2699 my $summarypage = '';
2700 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2703 $invoice_data{'summarypage'} = $summarypage;
2705 warn "$me substituting variables in notes, footer, smallfooter\n"
2708 my @include = (qw( notes footer smallfooter ));
2709 push @include, 'coupon' unless $params{'no_coupon'};
2710 foreach my $include (@include) {
2712 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2715 if ( $conf->exists($inc_file, $agentnum)
2716 && length( $conf->config($inc_file, $agentnum) ) ) {
2718 @inc_src = $conf->config($inc_file, $agentnum);
2722 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2724 my $convert_map = $convert_maps{$format}{$include};
2726 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2727 s/--\@\]/$delimiters{$format}[1]/g;
2730 &$convert_map( $conf->config($inc_file, $agentnum) );
2734 my $inc_tt = new Text::Template (
2736 SOURCE => [ map "$_\n", @inc_src ],
2737 DELIMITERS => $delimiters{$format},
2738 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2740 unless ( $inc_tt->compile() ) {
2741 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2742 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2746 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2748 $invoice_data{$include} =~ s/\n+$//
2749 if ($format eq 'latex');
2752 $invoice_data{'po_line'} =
2753 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2754 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2757 my %money_chars = ( 'latex' => '',
2758 'html' => $conf->config('money_char') || '$',
2761 my $money_char = $money_chars{$format};
2763 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2764 'html' => $conf->config('money_char') || '$',
2767 my $other_money_char = $other_money_chars{$format};
2768 $invoice_data{'dollar'} = $other_money_char;
2770 my @detail_items = ();
2771 my @total_items = ();
2775 $invoice_data{'detail_items'} = \@detail_items;
2776 $invoice_data{'total_items'} = \@total_items;
2777 $invoice_data{'buf'} = \@buf;
2778 $invoice_data{'sections'} = \@sections;
2780 warn "$me generating sections\n"
2783 my $previous_section = { 'description' => 'Previous Charges',
2784 'subtotal' => $other_money_char.
2785 sprintf('%.2f', $pr_total),
2786 'summarized' => $summarypage ? 'Y' : '',
2788 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2789 join(' / ', map { $cust_main->balance_date_range(@$_) }
2790 $self->_prior_month30s
2792 if $conf->exists('invoice_include_aging');
2795 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2796 'subtotal' => $taxtotal, # adjusted below
2797 'summarized' => $summarypage ? 'Y' : '',
2799 my $tax_weight = _pkg_category($tax_section->{description})
2800 ? _pkg_category($tax_section->{description})->weight
2802 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2803 $tax_section->{'sort_weight'} = $tax_weight;
2806 my $adjusttotal = 0;
2807 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2808 'subtotal' => 0, # adjusted below
2809 'summarized' => $summarypage ? 'Y' : '',
2811 my $adjust_weight = _pkg_category($adjust_section->{description})
2812 ? _pkg_category($adjust_section->{description})->weight
2814 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2815 $adjust_section->{'sort_weight'} = $adjust_weight;
2817 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2818 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2819 $invoice_data{'multisection'} = $multisection;
2820 my $late_sections = [];
2821 my $extra_sections = [];
2822 my $extra_lines = ();
2823 if ( $multisection ) {
2824 ($extra_sections, $extra_lines) =
2825 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2826 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2828 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2830 push @detail_items, @$extra_lines if $extra_lines;
2832 $self->_items_sections( $late_sections, # this could stand a refactor
2834 $escape_function_nonbsp,
2838 if ($conf->exists('svc_phone_sections')) {
2839 my ($phone_sections, $phone_lines) =
2840 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2841 push @{$late_sections}, @$phone_sections;
2842 push @detail_items, @$phone_lines;
2844 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2845 my ($accountcode_section, $accountcode_lines) =
2846 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2847 if ( scalar(@$accountcode_lines) ) {
2848 push @{$late_sections}, $accountcode_section;
2849 push @detail_items, @$accountcode_lines;
2853 push @sections, { 'description' => '', 'subtotal' => '' };
2856 unless ( $conf->exists('disable_previous_balance')
2857 || $conf->exists('previous_balance-summary_only')
2861 warn "$me adding previous balances\n"
2864 foreach my $line_item ( $self->_items_previous ) {
2867 ext_description => [],
2869 $detail->{'ref'} = $line_item->{'pkgnum'};
2870 $detail->{'quantity'} = 1;
2871 $detail->{'section'} = $previous_section;
2872 $detail->{'description'} = &$escape_function($line_item->{'description'});
2873 if ( exists $line_item->{'ext_description'} ) {
2874 @{$detail->{'ext_description'}} = map {
2875 &$escape_function($_);
2876 } @{$line_item->{'ext_description'}};
2878 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2879 $line_item->{'amount'};
2880 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2882 push @detail_items, $detail;
2883 push @buf, [ $detail->{'description'},
2884 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2890 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2891 push @buf, ['','-----------'];
2892 push @buf, [ 'Total Previous Balance',
2893 $money_char. sprintf("%10.2f", $pr_total) ];
2897 if ( $conf->exists('svc_phone-did-summary') ) {
2898 warn "$me adding DID summary\n"
2901 my ($didsummary,$minutes) = $self->_did_summary;
2902 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2904 { 'description' => $didsummary_desc,
2905 'ext_description' => [ $didsummary, $minutes ],
2910 foreach my $section (@sections, @$late_sections) {
2912 warn "$me adding section \n". Dumper($section)
2915 # begin some normalization
2916 $section->{'subtotal'} = $section->{'amount'}
2918 && !exists($section->{subtotal})
2919 && exists($section->{amount});
2921 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2922 if ( $invoice_data{finance_section} &&
2923 $section->{'description'} eq $invoice_data{finance_section} );
2925 $section->{'subtotal'} = $other_money_char.
2926 sprintf('%.2f', $section->{'subtotal'})
2929 # continue some normalization
2930 $section->{'amount'} = $section->{'subtotal'}
2934 if ( $section->{'description'} ) {
2935 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2940 warn "$me setting options\n"
2943 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2945 $options{'section'} = $section if $multisection;
2946 $options{'format'} = $format;
2947 $options{'escape_function'} = $escape_function;
2948 $options{'format_function'} = sub { () } unless $unsquelched;
2949 $options{'unsquelched'} = $unsquelched;
2950 $options{'summary_page'} = $summarypage;
2951 $options{'skip_usage'} =
2952 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2953 $options{'multilocation'} = $multilocation;
2954 $options{'multisection'} = $multisection;
2956 warn "$me searching for line items\n"
2959 foreach my $line_item ( $self->_items_pkg(%options) ) {
2961 warn "$me adding line item $line_item\n"
2965 ext_description => [],
2967 $detail->{'ref'} = $line_item->{'pkgnum'};
2968 $detail->{'quantity'} = $line_item->{'quantity'};
2969 $detail->{'section'} = $section;
2970 $detail->{'description'} = &$escape_function($line_item->{'description'});
2971 if ( exists $line_item->{'ext_description'} ) {
2972 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2974 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2975 $line_item->{'amount'};
2976 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2977 $line_item->{'unit_amount'};
2978 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2980 push @detail_items, $detail;
2981 push @buf, ( [ $detail->{'description'},
2982 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2984 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2988 if ( $section->{'description'} ) {
2989 push @buf, ( ['','-----------'],
2990 [ $section->{'description'}. ' sub-total',
2991 $money_char. sprintf("%10.2f", $section->{'subtotal'})
3000 $invoice_data{current_less_finance} =
3001 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3003 if ( $multisection && !$conf->exists('disable_previous_balance')
3004 || $conf->exists('previous_balance-summary_only') )
3006 unshift @sections, $previous_section if $pr_total;
3009 warn "$me adding taxes\n"
3012 foreach my $tax ( $self->_items_tax ) {
3014 $taxtotal += $tax->{'amount'};
3016 my $description = &$escape_function( $tax->{'description'} );
3017 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3019 if ( $multisection ) {
3021 my $money = $old_latex ? '' : $money_char;
3022 push @detail_items, {
3023 ext_description => [],
3026 description => $description,
3027 amount => $money. $amount,
3029 section => $tax_section,
3034 push @total_items, {
3035 'total_item' => $description,
3036 'total_amount' => $other_money_char. $amount,
3041 push @buf,[ $description,
3042 $money_char. $amount,
3049 $total->{'total_item'} = 'Sub-total';
3050 $total->{'total_amount'} =
3051 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3053 if ( $multisection ) {
3054 $tax_section->{'subtotal'} = $other_money_char.
3055 sprintf('%.2f', $taxtotal);
3056 $tax_section->{'pretotal'} = 'New charges sub-total '.
3057 $total->{'total_amount'};
3058 push @sections, $tax_section if $taxtotal;
3060 unshift @total_items, $total;
3063 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3065 push @buf,['','-----------'];
3066 push @buf,[( $conf->exists('disable_previous_balance')
3068 : 'Total New Charges'
3070 $money_char. sprintf("%10.2f",$self->charged) ];
3076 $item = $conf->config('previous_balance-exclude_from_total')
3077 || 'Total New Charges'
3078 if $conf->exists('previous_balance-exclude_from_total');
3079 my $amount = $self->charged +
3080 ( $conf->exists('disable_previous_balance') ||
3081 $conf->exists('previous_balance-exclude_from_total')
3085 $total->{'total_item'} = &$embolden_function($item);
3086 $total->{'total_amount'} =
3087 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3088 if ( $multisection ) {
3089 if ( $adjust_section->{'sort_weight'} ) {
3090 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3091 sprintf("%.2f", ($self->billing_balance || 0) );
3093 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3094 sprintf('%.2f', $self->charged );
3097 push @total_items, $total;
3099 push @buf,['','-----------'];
3102 sprintf( '%10.2f', $amount )
3107 unless ( $conf->exists('disable_previous_balance') ) {
3108 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3111 my $credittotal = 0;
3112 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3115 $total->{'total_item'} = &$escape_function($credit->{'description'});
3116 $credittotal += $credit->{'amount'};
3117 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3118 $adjusttotal += $credit->{'amount'};
3119 if ( $multisection ) {
3120 my $money = $old_latex ? '' : $money_char;
3121 push @detail_items, {
3122 ext_description => [],
3125 description => &$escape_function($credit->{'description'}),
3126 amount => $money. $credit->{'amount'},
3128 section => $adjust_section,
3131 push @total_items, $total;
3135 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3138 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3139 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3143 my $paymenttotal = 0;
3144 foreach my $payment ( $self->_items_payments ) {
3146 $total->{'total_item'} = &$escape_function($payment->{'description'});
3147 $paymenttotal += $payment->{'amount'};
3148 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3149 $adjusttotal += $payment->{'amount'};
3150 if ( $multisection ) {
3151 my $money = $old_latex ? '' : $money_char;
3152 push @detail_items, {
3153 ext_description => [],
3156 description => &$escape_function($payment->{'description'}),
3157 amount => $money. $payment->{'amount'},
3159 section => $adjust_section,
3162 push @total_items, $total;
3164 push @buf, [ $payment->{'description'},
3165 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3168 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3170 if ( $multisection ) {
3171 $adjust_section->{'subtotal'} = $other_money_char.
3172 sprintf('%.2f', $adjusttotal);
3173 push @sections, $adjust_section
3174 unless $adjust_section->{sort_weight};
3179 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3180 $total->{'total_amount'} =
3181 &$embolden_function(
3182 $other_money_char. sprintf('%.2f', $summarypage
3184 $self->billing_balance
3185 : $self->owed + $pr_total
3188 if ( $multisection && !$adjust_section->{sort_weight} ) {
3189 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3190 $total->{'total_amount'};
3192 push @total_items, $total;
3194 push @buf,['','-----------'];
3195 push @buf,[$self->balance_due_msg, $money_char.
3196 sprintf("%10.2f", $balance_due ) ];
3199 if ( $conf->exists('previous_balance-show_credit')
3200 and $cust_main->balance < 0 ) {
3201 my $credit_total = {
3202 'total_item' => &$embolden_function($self->credit_balance_msg),
3203 'total_amount' => &$embolden_function(
3204 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3207 if ( $multisection ) {
3208 $adjust_section->{'posttotal'} .= $newline_token .
3209 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3212 push @total_items, $credit_total;
3214 push @buf,['','-----------'];
3215 push @buf,[$self->credit_balance_msg, $money_char.
3216 sprintf("%10.2f", -$cust_main->balance ) ];
3220 if ( $multisection ) {
3221 if ($conf->exists('svc_phone_sections')) {
3223 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3224 $total->{'total_amount'} =
3225 &$embolden_function(
3226 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3228 my $last_section = pop @sections;
3229 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3230 $total->{'total_amount'};
3231 push @sections, $last_section;
3233 push @sections, @$late_sections
3237 my @includelist = ();
3238 push @includelist, 'summary' if $summarypage;
3239 foreach my $include ( @includelist ) {
3241 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3244 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3246 @inc_src = $conf->config($inc_file, $agentnum);
3250 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3252 my $convert_map = $convert_maps{$format}{$include};
3254 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3255 s/--\@\]/$delimiters{$format}[1]/g;
3258 &$convert_map( $conf->config($inc_file, $agentnum) );
3262 my $inc_tt = new Text::Template (
3264 SOURCE => [ map "$_\n", @inc_src ],
3265 DELIMITERS => $delimiters{$format},
3266 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3268 unless ( $inc_tt->compile() ) {
3269 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3270 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3274 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3276 $invoice_data{$include} =~ s/\n+$//
3277 if ($format eq 'latex');
3282 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3283 /invoice_lines\((\d*)\)/;
3284 $invoice_lines += $1 || scalar(@buf);
3287 die "no invoice_lines() functions in template?"
3288 if ( $format eq 'template' && !$wasfunc );
3290 if ($format eq 'template') {
3292 if ( $invoice_lines ) {
3293 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3294 $invoice_data{'total_pages'}++
3295 if scalar(@buf) % $invoice_lines;
3298 #setup subroutine for the template
3299 sub FS::cust_bill::_template::invoice_lines {
3300 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3302 scalar(@FS::cust_bill::_template::buf)
3303 ? shift @FS::cust_bill::_template::buf
3312 push @collect, split("\n",
3313 $text_template->fill_in( HASH => \%invoice_data,
3314 PACKAGE => 'FS::cust_bill::_template'
3317 $FS::cust_bill::_template::page++;
3319 map "$_\n", @collect;
3321 warn "filling in template for invoice ". $self->invnum. "\n"
3323 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3326 $text_template->fill_in(HASH => \%invoice_data);
3330 # helper routine for generating date ranges
3331 sub _prior_month30s {
3334 [ 1, 2592000 ], # 0-30 days ago
3335 [ 2592000, 5184000 ], # 30-60 days ago
3336 [ 5184000, 7776000 ], # 60-90 days ago
3337 [ 7776000, 0 ], # 90+ days ago
3340 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3341 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3346 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3348 Returns an postscript invoice, as a scalar.
3350 Options can be passed as a hashref (recommended) or as a list of time, template
3351 and then any key/value pairs for any other options.
3353 I<time> an optional value used to control the printing of overdue messages. The
3354 default is now. It isn't the date of the invoice; that's the `_date' field.
3355 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3356 L<Time::Local> and L<Date::Parse> for conversion functions.
3358 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3365 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3366 my $ps = generate_ps($file);
3368 unlink($barcodefile) if $barcodefile;
3373 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3375 Returns an PDF invoice, as a scalar.
3377 Options can be passed as a hashref (recommended) or as a list of time, template
3378 and then any key/value pairs for any other options.
3380 I<time> an optional value used to control the printing of overdue messages. The
3381 default is now. It isn't the date of the invoice; that's the `_date' field.
3382 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3383 L<Time::Local> and L<Date::Parse> for conversion functions.
3385 I<template>, if specified, is the name of a suffix for alternate invoices.
3387 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3394 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3395 my $pdf = generate_pdf($file);
3397 unlink($barcodefile) if $barcodefile;
3402 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3404 Returns an HTML invoice, as a scalar.
3406 I<time> an optional value used to control the printing of overdue messages. The
3407 default is now. It isn't the date of the invoice; that's the `_date' field.
3408 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3409 L<Time::Local> and L<Date::Parse> for conversion functions.
3411 I<template>, if specified, is the name of a suffix for alternate invoices.
3413 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3415 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3416 when emailing the invoice as part of a multipart/related MIME email.
3424 %params = %{ shift() };
3426 $params{'time'} = shift;
3427 $params{'template'} = shift;
3428 $params{'cid'} = shift;
3431 $params{'format'} = 'html';
3433 $self->print_generic( %params );
3436 # quick subroutine for print_latex
3438 # There are ten characters that LaTeX treats as special characters, which
3439 # means that they do not simply typeset themselves:
3440 # # $ % & ~ _ ^ \ { }
3442 # TeX ignores blanks following an escaped character; if you want a blank (as
3443 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3447 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3448 $value =~ s/([<>])/\$$1\$/g;
3454 encode_entities($value);
3458 sub _html_escape_nbsp {
3459 my $value = _html_escape(shift);
3460 $value =~ s/ +/ /g;
3464 #utility methods for print_*
3466 sub _translate_old_latex_format {
3467 warn "_translate_old_latex_format called\n"
3474 if ( $line =~ /^%%Detail\s*$/ ) {
3476 push @template, q![@--!,
3477 q! foreach my $_tr_line (@detail_items) {!,
3478 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3479 q! $_tr_line->{'description'} .= !,
3480 q! "\\tabularnewline\n~~".!,
3481 q! join( "\\tabularnewline\n~~",!,
3482 q! @{$_tr_line->{'ext_description'}}!,
3486 while ( ( my $line_item_line = shift )
3487 !~ /^%%EndDetail\s*$/ ) {
3488 $line_item_line =~ s/'/\\'/g; # nice LTS
3489 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3490 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3491 push @template, " \$OUT .= '$line_item_line';";
3494 push @template, '}',
3497 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3499 push @template, '[@--',
3500 ' foreach my $_tr_line (@total_items) {';
3502 while ( ( my $total_item_line = shift )
3503 !~ /^%%EndTotalDetails\s*$/ ) {
3504 $total_item_line =~ s/'/\\'/g; # nice LTS
3505 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3506 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3507 push @template, " \$OUT .= '$total_item_line';";
3510 push @template, '}',
3514 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3515 push @template, $line;
3521 warn "$_\n" foreach @template;
3530 #check for an invoice-specific override
3531 return $self->invoice_terms if $self->invoice_terms;
3533 #check for a customer- specific override
3534 my $cust_main = $self->cust_main;
3535 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3537 #use configured default
3538 $conf->config('invoice_default_terms') || '';
3544 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3545 $duedate = $self->_date() + ( $1 * 86400 );
3552 $self->due_date ? time2str(shift, $self->due_date) : '';
3555 sub balance_due_msg {
3557 my $msg = 'Balance Due';
3558 return $msg unless $self->terms;
3559 if ( $self->due_date ) {
3560 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3561 } elsif ( $self->terms ) {
3562 $msg .= ' - '. $self->terms;
3567 sub balance_due_date {
3570 if ( $conf->exists('invoice_default_terms')
3571 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3572 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3577 sub credit_balance_msg { 'Credit Balance Remaining' }
3579 =item invnum_date_pretty
3581 Returns a string with the invoice number and date, for example:
3582 "Invoice #54 (3/20/2008)"
3586 sub invnum_date_pretty {
3588 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3593 Returns a string with the date, for example: "3/20/2008"
3599 time2str($date_format, $self->_date);
3602 use vars qw(%pkg_category_cache);
3603 sub _items_sections {
3606 my $summarypage = shift;
3608 my $extra_sections = shift;
3612 my %late_subtotal = ();
3615 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3618 my $usage = $cust_bill_pkg->usage;
3620 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3621 next if ( $display->summary && $summarypage );
3623 my $section = $display->section;
3624 my $type = $display->type;
3626 $not_tax{$section} = 1
3627 unless $cust_bill_pkg->pkgnum == 0;
3629 if ( $display->post_total && !$summarypage ) {
3630 if (! $type || $type eq 'S') {
3631 $late_subtotal{$section} += $cust_bill_pkg->setup
3632 if $cust_bill_pkg->setup != 0;
3636 $late_subtotal{$section} += $cust_bill_pkg->recur
3637 if $cust_bill_pkg->recur != 0;
3640 if ($type && $type eq 'R') {
3641 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3642 if $cust_bill_pkg->recur != 0;
3645 if ($type && $type eq 'U') {
3646 $late_subtotal{$section} += $usage
3647 unless scalar(@$extra_sections);
3652 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3654 if (! $type || $type eq 'S') {
3655 $subtotal{$section} += $cust_bill_pkg->setup
3656 if $cust_bill_pkg->setup != 0;
3660 $subtotal{$section} += $cust_bill_pkg->recur
3661 if $cust_bill_pkg->recur != 0;
3664 if ($type && $type eq 'R') {
3665 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3666 if $cust_bill_pkg->recur != 0;
3669 if ($type && $type eq 'U') {
3670 $subtotal{$section} += $usage
3671 unless scalar(@$extra_sections);
3680 %pkg_category_cache = ();
3682 push @$late, map { { 'description' => &{$escape}($_),
3683 'subtotal' => $late_subtotal{$_},
3685 'sort_weight' => ( _pkg_category($_)
3686 ? _pkg_category($_)->weight
3689 ((_pkg_category($_) && _pkg_category($_)->condense)
3690 ? $self->_condense_section($format)
3694 sort _sectionsort keys %late_subtotal;
3697 if ( $summarypage ) {
3698 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3699 map { $_->categoryname } qsearch('pkg_category', {});
3700 push @sections, '' if exists($subtotal{''});
3702 @sections = keys %subtotal;
3705 my @early = map { { 'description' => &{$escape}($_),
3706 'subtotal' => $subtotal{$_},
3707 'summarized' => $not_tax{$_} ? '' : 'Y',
3708 'tax_section' => $not_tax{$_} ? '' : 'Y',
3709 'sort_weight' => ( _pkg_category($_)
3710 ? _pkg_category($_)->weight
3713 ((_pkg_category($_) && _pkg_category($_)->condense)
3714 ? $self->_condense_section($format)
3719 push @early, @$extra_sections if $extra_sections;
3721 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3725 #helper subs for above
3728 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3732 my $categoryname = shift;
3733 $pkg_category_cache{$categoryname} ||=
3734 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3737 my %condensed_format = (
3738 'label' => [ qw( Description Qty Amount ) ],
3740 sub { shift->{description} },
3741 sub { shift->{quantity} },
3742 sub { my($href, %opt) = @_;
3743 ($opt{dollar} || ''). $href->{amount};
3746 'align' => [ qw( l r r ) ],
3747 'span' => [ qw( 5 1 1 ) ], # unitprices?
3748 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3751 sub _condense_section {
3752 my ( $self, $format ) = ( shift, shift );
3754 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3755 qw( description_generator
3758 total_line_generator
3763 sub _condensed_generator_defaults {
3764 my ( $self, $format ) = ( shift, shift );
3765 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3774 sub _condensed_header_generator {
3775 my ( $self, $format ) = ( shift, shift );
3777 my ( $f, $prefix, $suffix, $separator, $column ) =
3778 _condensed_generator_defaults($format);
3780 if ($format eq 'latex') {
3781 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3782 $suffix = "\\\\\n\\hline";
3785 sub { my ($d,$a,$s,$w) = @_;
3786 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3788 } elsif ( $format eq 'html' ) {
3789 $prefix = '<th></th>';
3793 sub { my ($d,$a,$s,$w) = @_;
3794 return qq!<th align="$html_align{$a}">$d</th>!;
3802 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3804 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3807 $prefix. join($separator, @result). $suffix;
3812 sub _condensed_description_generator {
3813 my ( $self, $format ) = ( shift, shift );
3815 my ( $f, $prefix, $suffix, $separator, $column ) =
3816 _condensed_generator_defaults($format);
3818 my $money_char = '$';
3819 if ($format eq 'latex') {
3820 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3822 $separator = " & \n";
3824 sub { my ($d,$a,$s,$w) = @_;
3825 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3827 $money_char = '\\dollar';
3828 }elsif ( $format eq 'html' ) {
3829 $prefix = '"><td align="center"></td>';
3833 sub { my ($d,$a,$s,$w) = @_;
3834 return qq!<td align="$html_align{$a}">$d</td>!;
3836 #$money_char = $conf->config('money_char') || '$';
3837 $money_char = ''; # this is madness
3845 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3847 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3849 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3850 map { $f->{$_}->[$i] } qw(align span width)
3854 $prefix. join( $separator, @result ). $suffix;
3859 sub _condensed_total_generator {
3860 my ( $self, $format ) = ( shift, shift );
3862 my ( $f, $prefix, $suffix, $separator, $column ) =
3863 _condensed_generator_defaults($format);
3866 if ($format eq 'latex') {
3869 $separator = " & \n";
3871 sub { my ($d,$a,$s,$w) = @_;
3872 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3874 }elsif ( $format eq 'html' ) {
3878 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3880 sub { my ($d,$a,$s,$w) = @_;
3881 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3890 # my $r = &{$f->{fields}->[$i]}(@args);
3891 # $r .= ' Total' unless $i;
3893 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3895 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3896 map { $f->{$_}->[$i] } qw(align span width)
3900 $prefix. join( $separator, @result ). $suffix;
3905 =item total_line_generator FORMAT
3907 Returns a coderef used for generation of invoice total line items for this
3908 usage_class. FORMAT is either html or latex
3912 # should not be used: will have issues with hash element names (description vs
3913 # total_item and amount vs total_amount -- another array of functions?
3915 sub _condensed_total_line_generator {
3916 my ( $self, $format ) = ( shift, shift );
3918 my ( $f, $prefix, $suffix, $separator, $column ) =
3919 _condensed_generator_defaults($format);
3922 if ($format eq 'latex') {
3925 $separator = " & \n";
3927 sub { my ($d,$a,$s,$w) = @_;
3928 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3930 }elsif ( $format eq 'html' ) {
3934 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3936 sub { my ($d,$a,$s,$w) = @_;
3937 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3946 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3948 &{$column}( &{$f->{fields}->[$i]}(@args),
3949 map { $f->{$_}->[$i] } qw(align span width)
3953 $prefix. join( $separator, @result ). $suffix;
3958 #sub _items_extra_usage_sections {
3960 # my $escape = shift;
3962 # my %sections = ();
3964 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3965 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3967 # next unless $cust_bill_pkg->pkgnum > 0;
3969 # foreach my $section ( keys %usage_class ) {
3971 # my $usage = $cust_bill_pkg->usage($section);
3973 # next unless $usage && $usage > 0;
3975 # $sections{$section} ||= 0;
3976 # $sections{$section} += $usage;
3982 # map { { 'description' => &{$escape}($_),
3983 # 'subtotal' => $sections{$_},
3984 # 'summarized' => '',
3985 # 'tax_section' => '',
3988 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3992 sub _items_extra_usage_sections {
4001 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4002 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4003 next unless $cust_bill_pkg->pkgnum > 0;
4005 foreach my $classnum ( keys %usage_class ) {
4006 my $section = $usage_class{$classnum}->classname;
4007 $classnums{$section} = $classnum;
4009 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4010 my $amount = $detail->amount;
4011 next unless $amount && $amount > 0;
4013 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4014 $sections{$section}{amount} += $amount; #subtotal
4015 $sections{$section}{calls}++;
4016 $sections{$section}{duration} += $detail->duration;
4018 my $desc = $detail->regionname;
4019 my $description = $desc;
4020 $description = substr($desc, 0, 50). '...'
4021 if $format eq 'latex' && length($desc) > 50;
4023 $lines{$section}{$desc} ||= {
4024 description => &{$escape}($description),
4025 #pkgpart => $part_pkg->pkgpart,
4026 pkgnum => $cust_bill_pkg->pkgnum,
4031 #unit_amount => $cust_bill_pkg->unitrecur,
4032 quantity => $cust_bill_pkg->quantity,
4033 product_code => 'N/A',
4034 ext_description => [],
4037 $lines{$section}{$desc}{amount} += $amount;
4038 $lines{$section}{$desc}{calls}++;
4039 $lines{$section}{$desc}{duration} += $detail->duration;
4045 my %sectionmap = ();
4046 foreach (keys %sections) {
4047 my $usage_class = $usage_class{$classnums{$_}};
4048 $sectionmap{$_} = { 'description' => &{$escape}($_),
4049 'amount' => $sections{$_}{amount}, #subtotal
4050 'calls' => $sections{$_}{calls},
4051 'duration' => $sections{$_}{duration},
4053 'tax_section' => '',
4054 'sort_weight' => $usage_class->weight,
4055 ( $usage_class->format
4056 ? ( map { $_ => $usage_class->$_($format) }
4057 qw( description_generator header_generator total_generator total_line_generator )
4064 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4068 foreach my $section ( keys %lines ) {
4069 foreach my $line ( keys %{$lines{$section}} ) {
4070 my $l = $lines{$section}{$line};
4071 $l->{section} = $sectionmap{$section};
4072 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4073 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4078 return(\@sections, \@lines);
4084 my $end = $self->_date;
4086 # start at date of previous invoice + 1 second or 0 if no previous invoice
4087 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4088 $start = 0 if !$start;
4091 my $cust_main = $self->cust_main;
4092 my @pkgs = $cust_main->all_pkgs;
4093 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4096 foreach my $pkg ( @pkgs ) {
4097 my @h_cust_svc = $pkg->h_cust_svc($end);
4098 foreach my $h_cust_svc ( @h_cust_svc ) {
4099 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4100 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4102 my $inserted = $h_cust_svc->date_inserted;
4103 my $deleted = $h_cust_svc->date_deleted;
4104 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4106 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4108 # DID either activated or ported in; cannot be both for same DID simultaneously
4109 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4110 && (!$phone_inserted->lnp_status
4111 || $phone_inserted->lnp_status eq ''
4112 || $phone_inserted->lnp_status eq 'native')) {
4115 else { # this one not so clean, should probably move to (h_)svc_phone
4116 my $phone_portedin = qsearchs( 'h_svc_phone',
4117 { 'svcnum' => $h_cust_svc->svcnum,
4118 'lnp_status' => 'portedin' },
4119 FS::h_svc_phone->sql_h_searchs($end),
4121 $num_portedin++ if $phone_portedin;
4124 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4125 if($deleted >= $start && $deleted <= $end && $phone_deleted
4126 && (!$phone_deleted->lnp_status
4127 || $phone_deleted->lnp_status ne 'portingout')) {
4130 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4131 && $phone_deleted->lnp_status
4132 && $phone_deleted->lnp_status eq 'portingout') {
4136 # increment usage minutes
4137 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4138 foreach my $cdr ( @cdrs ) {
4139 $minutes += $cdr->billsec/60;
4142 # don't look at this service again
4143 push @seen, $h_cust_svc->svcnum;
4147 $minutes = sprintf("%d", $minutes);
4148 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4149 . "$num_deactivated Ported-Out: $num_portedout ",
4150 "Total Minutes: $minutes");
4153 sub _items_accountcode_cdr {
4158 my $section = { 'amount' => 0,
4161 'sort_weight' => '',
4163 'description' => 'Usage by Account Code',
4169 my %accountcodes = ();
4171 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4172 next unless $cust_bill_pkg->pkgnum > 0;
4174 my @header = $cust_bill_pkg->details_header;
4175 next unless scalar(@header);
4176 $section->{'header'} = join(',',@header);
4178 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4180 $section->{'header'} = $detail->formatted('format' => $format)
4181 if($detail->detail eq $section->{'header'});
4183 my $accountcode = $detail->accountcode;
4184 next unless $accountcode;
4186 my $amount = $detail->amount;
4187 next unless $amount && $amount > 0;
4189 $accountcodes{$accountcode} ||= {
4190 description => $accountcode,
4197 product_code => 'N/A',
4198 section => $section,
4199 ext_description => [],
4202 $section->{'amount'} += $amount;
4203 $accountcodes{$accountcode}{'amount'} += $amount;
4204 $accountcodes{$accountcode}{calls}++;
4205 $accountcodes{$accountcode}{duration} += $detail->duration;
4206 push @{$accountcodes{$accountcode}{ext_description}},
4207 $detail->formatted('format' => $format);
4211 foreach my $l ( values %accountcodes ) {
4212 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4213 unshift @{$l->{ext_description}}, $section->{'header'};
4217 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4219 return ($section,\@sorted_lines);
4222 sub _items_svc_phone_sections {
4231 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4232 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4234 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4235 next unless $cust_bill_pkg->pkgnum > 0;
4237 my @header = $cust_bill_pkg->details_header;
4238 next unless scalar(@header);
4240 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4242 my $phonenum = $detail->phonenum;
4243 next unless $phonenum;
4245 my $amount = $detail->amount;
4246 next unless $amount && $amount > 0;
4248 $sections{$phonenum} ||= { 'amount' => 0,
4251 'sort_weight' => -1,
4252 'phonenum' => $phonenum,
4254 $sections{$phonenum}{amount} += $amount; #subtotal
4255 $sections{$phonenum}{calls}++;
4256 $sections{$phonenum}{duration} += $detail->duration;
4258 my $desc = $detail->regionname;
4259 my $description = $desc;
4260 $description = substr($desc, 0, 50). '...'
4261 if $format eq 'latex' && length($desc) > 50;
4263 $lines{$phonenum}{$desc} ||= {
4264 description => &{$escape}($description),
4265 #pkgpart => $part_pkg->pkgpart,
4273 product_code => 'N/A',
4274 ext_description => [],
4277 $lines{$phonenum}{$desc}{amount} += $amount;
4278 $lines{$phonenum}{$desc}{calls}++;
4279 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4281 my $line = $usage_class{$detail->classnum}->classname;
4282 $sections{"$phonenum $line"} ||=
4286 'sort_weight' => $usage_class{$detail->classnum}->weight,
4287 'phonenum' => $phonenum,
4288 'header' => [ @header ],
4290 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4291 $sections{"$phonenum $line"}{calls}++;
4292 $sections{"$phonenum $line"}{duration} += $detail->duration;
4294 $lines{"$phonenum $line"}{$desc} ||= {
4295 description => &{$escape}($description),
4296 #pkgpart => $part_pkg->pkgpart,
4304 product_code => 'N/A',
4305 ext_description => [],
4308 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4309 $lines{"$phonenum $line"}{$desc}{calls}++;
4310 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4311 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4312 $detail->formatted('format' => $format);
4317 my %sectionmap = ();
4318 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4319 foreach ( keys %sections ) {
4320 my @header = @{ $sections{$_}{header} || [] };
4322 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4323 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4324 my $usage_class = $summary ? $simple : $usage_simple;
4325 my $ending = $summary ? ' usage charges' : '';
4328 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4330 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4331 'amount' => $sections{$_}{amount}, #subtotal
4332 'calls' => $sections{$_}{calls},
4333 'duration' => $sections{$_}{duration},
4335 'tax_section' => '',
4336 'phonenum' => $sections{$_}{phonenum},
4337 'sort_weight' => $sections{$_}{sort_weight},
4338 'post_total' => $summary, #inspire pagebreak
4340 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4341 qw( description_generator
4344 total_line_generator
4351 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4352 $a->{sort_weight} <=> $b->{sort_weight}
4357 foreach my $section ( keys %lines ) {
4358 foreach my $line ( keys %{$lines{$section}} ) {
4359 my $l = $lines{$section}{$line};
4360 $l->{section} = $sectionmap{$section};
4361 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4362 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4367 if($conf->exists('phone_usage_class_summary')) {
4368 # this only works with Latex
4372 # after this, we'll have only two sections per DID:
4373 # Calls Summary and Calls Detail
4374 foreach my $section ( @sections ) {
4375 if($section->{'post_total'}) {
4376 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4377 $section->{'total_line_generator'} = sub { '' };
4378 $section->{'total_generator'} = sub { '' };
4379 $section->{'header_generator'} = sub { '' };
4380 $section->{'description_generator'} = '';
4381 push @newsections, $section;
4382 my %calls_detail = %$section;
4383 $calls_detail{'post_total'} = '';
4384 $calls_detail{'sort_weight'} = '';
4385 $calls_detail{'description_generator'} = sub { '' };
4386 $calls_detail{'header_generator'} = sub {
4387 return ' & Date/Time & Called Number & Duration & Price'
4388 if $format eq 'latex';
4391 $calls_detail{'description'} = 'Calls Detail: '
4392 . $section->{'phonenum'};
4393 push @newsections, \%calls_detail;
4397 # after this, each usage class is collapsed/summarized into a single
4398 # line under the Calls Summary section
4399 foreach my $newsection ( @newsections ) {
4400 if($newsection->{'post_total'}) { # this means Calls Summary
4401 foreach my $section ( @sections ) {
4402 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4403 && !$section->{'post_total'});
4404 my $newdesc = $section->{'description'};
4405 my $tn = $section->{'phonenum'};
4406 $newdesc =~ s/$tn//g;
4407 my $line = { ext_description => [],
4411 calls => $section->{'calls'},
4412 section => $newsection,
4413 duration => $section->{'duration'},
4414 description => $newdesc,
4415 amount => sprintf("%.2f",$section->{'amount'}),
4416 product_code => 'N/A',
4418 push @newlines, $line;
4423 # after this, Calls Details is populated with all CDRs
4424 foreach my $newsection ( @newsections ) {
4425 if(!$newsection->{'post_total'}) { # this means Calls Details
4426 foreach my $line ( @lines ) {
4427 next unless (scalar(@{$line->{'ext_description'}}) &&
4428 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4430 my @extdesc = @{$line->{'ext_description'}};
4432 foreach my $extdesc ( @extdesc ) {
4433 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4434 push @newextdesc, $extdesc;
4436 $line->{'ext_description'} = \@newextdesc;
4437 $line->{'section'} = $newsection;
4438 push @newlines, $line;
4443 return(\@newsections, \@newlines);
4446 return(\@sections, \@lines);
4453 #my @display = scalar(@_)
4455 # : qw( _items_previous _items_pkg );
4456 # #: qw( _items_pkg );
4457 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4458 my @display = qw( _items_previous _items_pkg );
4461 foreach my $display ( @display ) {
4462 push @b, $self->$display(@_);
4467 sub _items_previous {
4469 my $cust_main = $self->cust_main;
4470 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4472 foreach ( @pr_cust_bill ) {
4473 my $date = $conf->exists('invoice_show_prior_due_date')
4474 ? 'due '. $_->due_date2str($date_format)
4475 : time2str($date_format, $_->_date);
4477 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4478 #'pkgpart' => 'N/A',
4480 'amount' => sprintf("%.2f", $_->owed),
4486 # 'description' => 'Previous Balance',
4487 # #'pkgpart' => 'N/A',
4488 # 'pkgnum' => 'N/A',
4489 # 'amount' => sprintf("%10.2f", $pr_total ),
4490 # 'ext_description' => [ map {
4491 # "Invoice ". $_->invnum.
4492 # " (". time2str("%x",$_->_date). ") ".
4493 # sprintf("%10.2f", $_->owed)
4494 # } @pr_cust_bill ],
4503 warn "$me _items_pkg searching for all package line items\n"
4506 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4508 warn "$me _items_pkg filtering line items\n"
4510 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4512 if ($options{section} && $options{section}->{condensed}) {
4514 warn "$me _items_pkg condensing section\n"
4518 local $Storable::canonical = 1;
4519 foreach ( @items ) {
4521 delete $item->{ref};
4522 delete $item->{ext_description};
4523 my $key = freeze($item);
4524 $itemshash{$key} ||= 0;
4525 $itemshash{$key} ++; # += $item->{quantity};
4527 @items = sort { $a->{description} cmp $b->{description} }
4528 map { my $i = thaw($_);
4529 $i->{quantity} = $itemshash{$_};
4531 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4537 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4544 return 0 unless $a->itemdesc cmp $b->itemdesc;
4545 return -1 if $b->itemdesc eq 'Tax';
4546 return 1 if $a->itemdesc eq 'Tax';
4547 return -1 if $b->itemdesc eq 'Other surcharges';
4548 return 1 if $a->itemdesc eq 'Other surcharges';
4549 $a->itemdesc cmp $b->itemdesc;
4554 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4555 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4558 sub _items_cust_bill_pkg {
4560 my $cust_bill_pkgs = shift;
4563 my $format = $opt{format} || '';
4564 my $escape_function = $opt{escape_function} || sub { shift };
4565 my $format_function = $opt{format_function} || '';
4566 my $unsquelched = $opt{unsquelched} || '';
4567 my $section = $opt{section}->{description} if $opt{section};
4568 my $summary_page = $opt{summary_page} || '';
4569 my $multilocation = $opt{multilocation} || '';
4570 my $multisection = $opt{multisection} || '';
4571 my $discount_show_always = 0;
4574 my ($s, $r, $u) = ( undef, undef, undef );
4575 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4578 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4579 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4582 foreach my $display ( grep { defined($section)
4583 ? $_->section eq $section
4586 #grep { !$_->summary || !$summary_page } # bunk!
4587 grep { !$_->summary || $multisection }
4588 $cust_bill_pkg->cust_bill_pkg_display
4592 warn "$me _items_cust_bill_pkg considering display item $display\n"
4595 my $type = $display->type;
4597 my $desc = $cust_bill_pkg->desc;
4598 $desc = substr($desc, 0, 50). '...'
4599 if $format eq 'latex' && length($desc) > 50;
4601 my %details_opt = ( 'format' => $format,
4602 'escape_function' => $escape_function,
4603 'format_function' => $format_function,
4606 if ( $cust_bill_pkg->pkgnum > 0 ) {
4608 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4611 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4613 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4615 warn "$me _items_cust_bill_pkg adding setup\n"
4618 my $description = $desc;
4619 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4622 unless ( $cust_pkg->part_pkg->hide_svc_detail
4623 || $cust_bill_pkg->hidden )
4626 push @d, map &{$escape_function}($_),
4627 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4628 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4630 if ( $multilocation ) {
4631 my $loc = $cust_pkg->location_label;
4632 $loc = substr($loc, 0, 50). '...'
4633 if $format eq 'latex' && length($loc) > 50;
4634 push @d, &{$escape_function}($loc);
4639 push @d, $cust_bill_pkg->details(%details_opt)
4640 if $cust_bill_pkg->recur == 0;
4642 if ( $cust_bill_pkg->hidden ) {
4643 $s->{amount} += $cust_bill_pkg->setup;
4644 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4645 push @{ $s->{ext_description} }, @d;
4648 description => $description,
4649 #pkgpart => $part_pkg->pkgpart,
4650 pkgnum => $cust_bill_pkg->pkgnum,
4651 amount => $cust_bill_pkg->setup,
4652 unit_amount => $cust_bill_pkg->unitsetup,
4653 quantity => $cust_bill_pkg->quantity,
4654 ext_description => \@d,
4660 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4662 $cust_bill_pkg->recur != 0
4663 || $cust_bill_pkg->setup == 0
4664 || $discount_show_always
4665 || $cust_bill_pkg->recur_show_zero
4670 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4673 my $is_summary = $display->summary;
4674 my $description = ($is_summary && $type && $type eq 'U')
4675 ? "Usage charges" : $desc;
4677 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4678 " - ". time2str($date_format, $cust_bill_pkg->edate).
4680 unless $conf->exists('disable_line_item_date_ranges');
4684 #at least until cust_bill_pkg has "past" ranges in addition to
4685 #the "future" sdate/edate ones... see #3032
4686 my @dates = ( $self->_date );
4687 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4688 push @dates, $prev->sdate if $prev;
4689 push @dates, undef if !$prev;
4691 unless ( $cust_pkg->part_pkg->hide_svc_detail
4692 || $cust_bill_pkg->itemdesc
4693 || $cust_bill_pkg->hidden
4694 || $is_summary && $type && $type eq 'U' )
4697 warn "$me _items_cust_bill_pkg adding service details\n"
4700 push @d, map &{$escape_function}($_),
4701 $cust_pkg->h_labels_short(@dates, 'I')
4702 #$cust_bill_pkg->edate,
4703 #$cust_bill_pkg->sdate)
4704 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4706 warn "$me _items_cust_bill_pkg done adding service details\n"
4709 if ( $multilocation ) {
4710 my $loc = $cust_pkg->location_label;
4711 $loc = substr($loc, 0, 50). '...'
4712 if $format eq 'latex' && length($loc) > 50;
4713 push @d, &{$escape_function}($loc);
4718 unless ( $is_summary ) {
4719 warn "$me _items_cust_bill_pkg adding details\n"
4722 #instead of omitting details entirely in this case (unwanted side
4723 # effects), just omit CDRs
4724 $details_opt{'format_function'} = sub { () }
4725 if $type && $type eq 'R';
4727 push @d, $cust_bill_pkg->details(%details_opt);
4730 warn "$me _items_cust_bill_pkg calculating amount\n"
4735 $amount = $cust_bill_pkg->recur;
4736 } elsif ($type eq 'R') {
4737 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4738 } elsif ($type eq 'U') {
4739 $amount = $cust_bill_pkg->usage;
4742 if ( !$type || $type eq 'R' ) {
4744 warn "$me _items_cust_bill_pkg adding recur\n"
4747 if ( $cust_bill_pkg->hidden ) {
4748 $r->{amount} += $amount;
4749 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4750 push @{ $r->{ext_description} }, @d;
4753 description => $description,
4754 #pkgpart => $part_pkg->pkgpart,
4755 pkgnum => $cust_bill_pkg->pkgnum,
4757 unit_amount => $cust_bill_pkg->unitrecur,
4758 quantity => $cust_bill_pkg->quantity,
4759 ext_description => \@d,
4763 } else { # $type eq 'U'
4765 warn "$me _items_cust_bill_pkg adding usage\n"
4768 if ( $cust_bill_pkg->hidden ) {
4769 $u->{amount} += $amount;
4770 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4771 push @{ $u->{ext_description} }, @d;
4774 description => $description,
4775 #pkgpart => $part_pkg->pkgpart,
4776 pkgnum => $cust_bill_pkg->pkgnum,
4778 unit_amount => $cust_bill_pkg->unitrecur,
4779 quantity => $cust_bill_pkg->quantity,
4780 ext_description => \@d,
4785 } # recurring or usage with recurring charge
4787 } else { #pkgnum tax or one-shot line item (??)
4789 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4792 if ( $cust_bill_pkg->setup != 0 ) {
4794 'description' => $desc,
4795 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4798 if ( $cust_bill_pkg->recur != 0 ) {
4800 'description' => "$desc (".
4801 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4802 time2str($date_format, $cust_bill_pkg->edate). ')',
4803 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4811 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4812 && $conf->exists('discount-show-always'));
4814 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4815 if ( $_ && !$cust_bill_pkg->hidden ) {
4816 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4817 $_->{amount} =~ s/^\-0\.00$/0.00/;
4818 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4820 if $_->{amount} != 0
4821 || $discount_show_always
4822 || $cust_bill_pkg->recur_show_zero;
4829 #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4831 # $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4832 # $_->{amount} =~ s/^\-0\.00$/0.00/;
4833 # $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4835 # if $_->{amount} != 0
4836 # || $discount_show_always
4840 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4847 sub _items_credits {
4848 my( $self, %opt ) = @_;
4849 my $trim_len = $opt{'trim_len'} || 60;
4853 foreach ( $self->cust_credited ) {
4855 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4857 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4858 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4859 $reason = " ($reason) " if $reason;
4862 #'description' => 'Credit ref\#'. $_->crednum.
4863 # " (". time2str("%x",$_->cust_credit->_date) .")".
4865 'description' => 'Credit applied '.
4866 time2str($date_format,$_->cust_credit->_date). $reason,
4867 'amount' => sprintf("%.2f",$_->amount),
4875 sub _items_payments {
4879 #get & print payments
4880 foreach ( $self->cust_bill_pay ) {
4882 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4885 'description' => "Payment received ".
4886 time2str($date_format,$_->cust_pay->_date ),
4887 'amount' => sprintf("%.2f", $_->amount )
4895 =item call_details [ OPTION => VALUE ... ]
4897 Returns an array of CSV strings representing the call details for this invoice
4898 The only option available is the boolean prepend_billed_number
4903 my ($self, %opt) = @_;
4905 my $format_function = sub { shift };
4907 if ($opt{prepend_billed_number}) {
4908 $format_function = sub {
4912 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4917 my @details = map { $_->details( 'format_function' => $format_function,
4918 'escape_function' => sub{ return() },
4922 $self->cust_bill_pkg;
4923 my $header = $details[0];
4924 ( $header, grep { $_ ne $header } @details );
4934 =item process_reprint
4938 sub process_reprint {
4939 process_re_X('print', @_);
4942 =item process_reemail
4946 sub process_reemail {
4947 process_re_X('email', @_);
4955 process_re_X('fax', @_);
4963 process_re_X('ftp', @_);
4970 sub process_respool {
4971 process_re_X('spool', @_);
4974 use Storable qw(thaw);
4978 my( $method, $job ) = ( shift, shift );
4979 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4981 my $param = thaw(decode_base64(shift));
4982 warn Dumper($param) if $DEBUG;
4993 my($method, $job, %param ) = @_;
4995 warn "re_X $method for job $job with param:\n".
4996 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4999 #some false laziness w/search/cust_bill.html
5001 my $orderby = 'ORDER BY cust_bill._date';
5003 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5005 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5007 my @cust_bill = qsearch( {
5008 #'select' => "cust_bill.*",
5009 'table' => 'cust_bill',
5010 'addl_from' => $addl_from,
5012 'extra_sql' => $extra_sql,
5013 'order_by' => $orderby,
5017 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5019 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5022 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5023 foreach my $cust_bill ( @cust_bill ) {
5024 $cust_bill->$method();
5026 if ( $job ) { #progressbar foo
5028 if ( time - $min_sec > $last ) {
5029 my $error = $job->update_statustext(
5030 int( 100 * $num / scalar(@cust_bill) )
5032 die $error if $error;
5043 =head1 CLASS METHODS
5049 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5054 my ($class, $start, $end) = @_;
5056 $class->paid_sql($start, $end). ' - '.
5057 $class->credited_sql($start, $end);
5062 Returns an SQL fragment to retreive the net amount (charged minus credited).
5067 my ($class, $start, $end) = @_;
5068 'charged - '. $class->credited_sql($start, $end);
5073 Returns an SQL fragment to retreive the amount paid against this invoice.
5078 my ($class, $start, $end) = @_;
5079 $start &&= "AND cust_bill_pay._date <= $start";
5080 $end &&= "AND cust_bill_pay._date > $end";
5081 $start = '' unless defined($start);
5082 $end = '' unless defined($end);
5083 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5084 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5089 Returns an SQL fragment to retreive the amount credited against this invoice.
5094 my ($class, $start, $end) = @_;
5095 $start &&= "AND cust_credit_bill._date <= $start";
5096 $end &&= "AND cust_credit_bill._date > $end";
5097 $start = '' unless defined($start);
5098 $end = '' unless defined($end);
5099 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5100 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5105 Returns an SQL fragment to retrieve the due date of an invoice.
5106 Currently only supported on PostgreSQL.
5114 cust_bill.invoice_terms,
5115 cust_main.invoice_terms,
5116 \''.($conf->config('invoice_default_terms') || '').'\'
5117 ), E\'Net (\\\\d+)\'
5119 ) * 86400 + cust_bill._date'
5122 =item search_sql_where HASHREF
5124 Class method which returns an SQL WHERE fragment to search for parameters
5125 specified in HASHREF. Valid parameters are
5131 List reference of start date, end date, as UNIX timestamps.
5141 List reference of charged limits (exclusive).
5145 List reference of charged limits (exclusive).
5149 flag, return open invoices only
5153 flag, return net invoices only
5157 =item newest_percust
5161 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5165 sub search_sql_where {
5166 my($class, $param) = @_;
5168 warn "$me search_sql_where called with params: \n".
5169 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5175 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5176 push @search, "cust_main.agentnum = $1";
5180 if ( $param->{_date} ) {
5181 my($beginning, $ending) = @{$param->{_date}};
5183 push @search, "cust_bill._date >= $beginning",
5184 "cust_bill._date < $ending";
5188 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5189 push @search, "cust_bill.invnum >= $1";
5191 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5192 push @search, "cust_bill.invnum <= $1";
5196 if ( $param->{charged} ) {
5197 my @charged = ref($param->{charged})
5198 ? @{ $param->{charged} }
5199 : ($param->{charged});
5201 push @search, map { s/^charged/cust_bill.charged/; $_; }
5205 my $owed_sql = FS::cust_bill->owed_sql;
5208 if ( $param->{owed} ) {
5209 my @owed = ref($param->{owed})
5210 ? @{ $param->{owed} }
5212 push @search, map { s/^owed/$owed_sql/; $_; }
5217 push @search, "0 != $owed_sql"
5218 if $param->{'open'};
5219 push @search, '0 != '. FS::cust_bill->net_sql
5223 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5224 if $param->{'days'};
5227 if ( $param->{'newest_percust'} ) {
5229 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5230 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5232 my @newest_where = map { my $x = $_;
5233 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5236 grep ! /^cust_main./, @search;
5237 my $newest_where = scalar(@newest_where)
5238 ? ' AND '. join(' AND ', @newest_where)
5242 push @search, "cust_bill._date = (
5243 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5244 WHERE newest_cust_bill.custnum = cust_bill.custnum
5250 #agent virtualization
5251 my $curuser = $FS::CurrentUser::CurrentUser;
5252 if ( $curuser->username eq 'fs_queue'
5253 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5255 my $newuser = qsearchs('access_user', {
5256 'username' => $username,
5260 $curuser = $newuser;
5262 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5265 push @search, $curuser->agentnums_sql;
5267 join(' AND ', @search );
5279 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5280 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base