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 $bill_batch = $self->get_open_bill_batch;
1524 my $cust_bill_batch = FS::cust_bill_batch->new({
1525 batchnum => $bill_batch->batchnum,
1526 invnum => $self->invnum,
1528 return $cust_bill_batch->insert($opt);
1531 =item get_open_batch
1533 Returns the currently open batch as an FS::bill_batch object, creating a new
1534 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1539 sub get_open_bill_batch {
1541 my $hashref = { status => 'O' };
1542 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1543 ? $self->cust_main->agentnum
1545 my $batch = qsearchs('bill_batch', $hashref);
1546 return $batch if $batch;
1547 $batch = FS::bill_batch->new($hashref);
1548 my $error = $batch->insert;
1549 die $error if $error;
1553 =item ftp_invoice [ TEMPLATENAME ]
1555 Sends this invoice data via FTP.
1557 TEMPLATENAME is unused?
1563 my $template = scalar(@_) ? shift : '';
1566 'protocol' => 'ftp',
1567 'server' => $conf->config('cust_bill-ftpserver'),
1568 'username' => $conf->config('cust_bill-ftpusername'),
1569 'password' => $conf->config('cust_bill-ftppassword'),
1570 'dir' => $conf->config('cust_bill-ftpdir'),
1571 'format' => $conf->config('cust_bill-ftpformat'),
1575 =item spool_invoice [ TEMPLATENAME ]
1577 Spools this invoice data (see L<FS::spool_csv>)
1579 TEMPLATENAME is unused?
1585 my $template = scalar(@_) ? shift : '';
1588 'format' => $conf->config('cust_bill-spoolformat'),
1589 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1593 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1595 Like B<send>, but only sends the invoice if it is the newest open invoice for
1600 sub send_if_newest {
1605 grep { $_->owed > 0 }
1606 qsearch('cust_bill', {
1607 'custnum' => $self->custnum,
1608 #'_date' => { op=>'>', value=>$self->_date },
1609 'invnum' => { op=>'>', value=>$self->invnum },
1616 =item send_csv OPTION => VALUE, ...
1618 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1622 protocol - currently only "ftp"
1628 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1629 and YYMMDDHHMMSS is a timestamp.
1631 See L</print_csv> for a description of the output format.
1636 my($self, %opt) = @_;
1640 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1641 mkdir $spooldir, 0700 unless -d $spooldir;
1643 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1644 my $file = "$spooldir/$tracctnum.csv";
1646 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1648 open(CSV, ">$file") or die "can't open $file: $!";
1656 if ( $opt{protocol} eq 'ftp' ) {
1657 eval "use Net::FTP;";
1659 $net = Net::FTP->new($opt{server}) or die @$;
1661 die "unknown protocol: $opt{protocol}";
1664 $net->login( $opt{username}, $opt{password} )
1665 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1667 $net->binary or die "can't set binary mode";
1669 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1671 $net->put($file) or die "can't put $file: $!";
1681 Spools CSV invoice data.
1687 =item format - 'default' or 'billco'
1689 =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>).
1691 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1693 =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.
1700 my($self, %opt) = @_;
1702 my $cust_main = $self->cust_main;
1704 if ( $opt{'dest'} ) {
1705 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1706 $cust_main->invoicing_list;
1707 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1708 || ! keys %invoicing_list;
1711 if ( $opt{'balanceover'} ) {
1713 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1716 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1717 mkdir $spooldir, 0700 unless -d $spooldir;
1719 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1723 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1724 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1727 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1729 open(CSV, ">>$file") or die "can't open $file: $!";
1730 flock(CSV, LOCK_EX);
1735 if ( lc($opt{'format'}) eq 'billco' ) {
1737 flock(CSV, LOCK_UN);
1742 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1745 open(CSV,">>$file") or die "can't open $file: $!";
1746 flock(CSV, LOCK_EX);
1752 flock(CSV, LOCK_UN);
1759 =item print_csv OPTION => VALUE, ...
1761 Returns CSV data for this invoice.
1765 format - 'default' or 'billco'
1767 Returns a list consisting of two scalars. The first is a single line of CSV
1768 header information for this invoice. The second is one or more lines of CSV
1769 detail information for this invoice.
1771 If I<format> is not specified or "default", the fields of the CSV file are as
1774 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1778 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1780 B<record_type> is C<cust_bill> for the initial header line only. The
1781 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1782 fields are filled in.
1784 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1785 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1788 =item invnum - invoice number
1790 =item custnum - customer number
1792 =item _date - invoice date
1794 =item charged - total invoice amount
1796 =item first - customer first name
1798 =item last - customer first name
1800 =item company - company name
1802 =item address1 - address line 1
1804 =item address2 - address line 1
1814 =item pkg - line item description
1816 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1818 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1820 =item sdate - start date for recurring fee
1822 =item edate - end date for recurring fee
1826 If I<format> is "billco", the fields of the header CSV file are as follows:
1828 +-------------------------------------------------------------------+
1829 | FORMAT HEADER FILE |
1830 |-------------------------------------------------------------------|
1831 | Field | Description | Name | Type | Width |
1832 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1833 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1834 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1835 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1836 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1837 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1838 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1839 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1840 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1841 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1842 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1843 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1844 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1845 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1846 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1847 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1848 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1849 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1850 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1851 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1852 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1853 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1854 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1855 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1856 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1857 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1858 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1859 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1860 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1861 +-------+-------------------------------+------------+------+-------+
1863 If I<format> is "billco", the fields of the detail CSV file are as follows:
1865 FORMAT FOR DETAIL FILE
1867 Field | Description | Name | Type | Width
1868 1 | N/A-Leave Empty | RC | CHAR | 2
1869 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1870 3 | Account Number | TRACCTNUM | CHAR | 15
1871 4 | Invoice Number | TRINVOICE | CHAR | 15
1872 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1873 6 | Transaction Detail | DETAILS | CHAR | 100
1874 7 | Amount | AMT | NUM* | 9
1875 8 | Line Format Control** | LNCTRL | CHAR | 2
1876 9 | Grouping Code | GROUP | CHAR | 2
1877 10 | User Defined | ACCT CODE | CHAR | 15
1882 my($self, %opt) = @_;
1884 eval "use Text::CSV_XS";
1887 my $cust_main = $self->cust_main;
1889 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1891 if ( lc($opt{'format'}) eq 'billco' ) {
1894 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1896 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1898 my( $previous_balance, @unused ) = $self->previous; #previous balance
1900 my $pmt_cr_applied = 0;
1901 $pmt_cr_applied += $_->{'amount'}
1902 foreach ( $self->_items_payments, $self->_items_credits ) ;
1904 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1907 '', # 1 | N/A-Leave Empty CHAR 2
1908 '', # 2 | N/A-Leave Empty CHAR 15
1909 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1910 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1911 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1912 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1913 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1914 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1915 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1916 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1917 '', # 10 | Ancillary Billing Information CHAR 30
1918 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1919 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1922 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1925 $duedate, # 14 | Bill Due Date CHAR 10
1927 $previous_balance, # 15 | Previous Balance NUM* 9
1928 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1929 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1930 $totaldue, # 18 | Total Amt Due NUM* 9
1931 $totaldue, # 19 | Total Amt Due NUM* 9
1932 '', # 20 | 30 Day Aging NUM* 9
1933 '', # 21 | 60 Day Aging NUM* 9
1934 '', # 22 | 90 Day Aging NUM* 9
1935 'N', # 23 | Y/N CHAR 1
1936 '', # 24 | Remittance automation CHAR 100
1937 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1938 $self->custnum, # 26 | Customer Reference Number CHAR 15
1939 '0', # 27 | Federal Tax*** NUM* 9
1940 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1941 '0', # 29 | Other Taxes & Fees*** NUM* 9
1950 time2str("%x", $self->_date),
1951 sprintf("%.2f", $self->charged),
1952 ( map { $cust_main->getfield($_) }
1953 qw( first last company address1 address2 city state zip country ) ),
1955 ) or die "can't create csv";
1958 my $header = $csv->string. "\n";
1961 if ( lc($opt{'format'}) eq 'billco' ) {
1964 foreach my $item ( $self->_items_pkg ) {
1967 '', # 1 | N/A-Leave Empty CHAR 2
1968 '', # 2 | N/A-Leave Empty CHAR 15
1969 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1970 $self->invnum, # 4 | Invoice Number CHAR 15
1971 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1972 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1973 $item->{'amount'}, # 7 | Amount NUM* 9
1974 '', # 8 | Line Format Control** CHAR 2
1975 '', # 9 | Grouping Code CHAR 2
1976 '', # 10 | User Defined CHAR 15
1979 $detail .= $csv->string. "\n";
1985 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1987 my($pkg, $setup, $recur, $sdate, $edate);
1988 if ( $cust_bill_pkg->pkgnum ) {
1990 ($pkg, $setup, $recur, $sdate, $edate) = (
1991 $cust_bill_pkg->part_pkg->pkg,
1992 ( $cust_bill_pkg->setup != 0
1993 ? sprintf("%.2f", $cust_bill_pkg->setup )
1995 ( $cust_bill_pkg->recur != 0
1996 ? sprintf("%.2f", $cust_bill_pkg->recur )
1998 ( $cust_bill_pkg->sdate
1999 ? time2str("%x", $cust_bill_pkg->sdate)
2001 ($cust_bill_pkg->edate
2002 ?time2str("%x", $cust_bill_pkg->edate)
2006 } else { #pkgnum tax
2007 next unless $cust_bill_pkg->setup != 0;
2008 $pkg = $cust_bill_pkg->desc;
2009 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2010 ( $sdate, $edate ) = ( '', '' );
2016 ( map { '' } (1..11) ),
2017 ($pkg, $setup, $recur, $sdate, $edate)
2018 ) or die "can't create csv";
2020 $detail .= $csv->string. "\n";
2026 ( $header, $detail );
2032 Pays this invoice with a compliemntary payment. If there is an error,
2033 returns the error, otherwise returns false.
2039 my $cust_pay = new FS::cust_pay ( {
2040 'invnum' => $self->invnum,
2041 'paid' => $self->owed,
2044 'payinfo' => $self->cust_main->payinfo,
2052 Attempts to pay this invoice with a credit card payment via a
2053 Business::OnlinePayment realtime gateway. See
2054 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2055 for supported processors.
2061 $self->realtime_bop( 'CC', @_ );
2066 Attempts to pay this invoice with an electronic check (ACH) payment via a
2067 Business::OnlinePayment realtime gateway. See
2068 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2069 for supported processors.
2075 $self->realtime_bop( 'ECHECK', @_ );
2080 Attempts to pay this invoice with phone bill (LEC) payment via a
2081 Business::OnlinePayment realtime gateway. See
2082 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2083 for supported processors.
2089 $self->realtime_bop( 'LEC', @_ );
2093 my( $self, $method ) = (shift,shift);
2096 my $cust_main = $self->cust_main;
2097 my $balance = $cust_main->balance;
2098 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2099 $amount = sprintf("%.2f", $amount);
2100 return "not run (balance $balance)" unless $amount > 0;
2102 my $description = 'Internet Services';
2103 if ( $conf->exists('business-onlinepayment-description') ) {
2104 my $dtempl = $conf->config('business-onlinepayment-description');
2106 my $agent_obj = $cust_main->agent
2107 or die "can't retreive agent for $cust_main (agentnum ".
2108 $cust_main->agentnum. ")";
2109 my $agent = $agent_obj->agent;
2110 my $pkgs = join(', ',
2111 map { $_->part_pkg->pkg }
2112 grep { $_->pkgnum } $self->cust_bill_pkg
2114 $description = eval qq("$dtempl");
2117 $cust_main->realtime_bop($method, $amount,
2118 'description' => $description,
2119 'invnum' => $self->invnum,
2120 #this didn't do what we want, it just calls apply_payments_and_credits
2122 'apply_to_invoice' => 1,
2125 #this changes application behavior: auto payments
2126 #triggered against a specific invoice are now applied
2127 #to that invoice instead of oldest open.
2133 =item batch_card OPTION => VALUE...
2135 Adds a payment for this invoice to the pending credit card batch (see
2136 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2137 runs the payment using a realtime gateway.
2142 my ($self, %options) = @_;
2143 my $cust_main = $self->cust_main;
2145 $options{invnum} = $self->invnum;
2147 $cust_main->batch_card(%options);
2150 sub _agent_template {
2152 $self->cust_main->agent_template;
2155 sub _agent_invoice_from {
2157 $self->cust_main->agent_invoice_from;
2160 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2162 Returns an text invoice, as a list of lines.
2164 Options can be passed as a hashref (recommended) or as a list of time, template
2165 and then any key/value pairs for any other options.
2167 I<time>, if specified, is used to control the printing of overdue messages. The
2168 default is now. It isn't the date of the invoice; that's the `_date' field.
2169 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2170 L<Time::Local> and L<Date::Parse> for conversion functions.
2172 I<template>, if specified, is the name of a suffix for alternate invoices.
2174 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2180 my( $today, $template, %opt );
2182 %opt = %{ shift() };
2183 $today = delete($opt{'time'}) || '';
2184 $template = delete($opt{template}) || '';
2186 ( $today, $template, %opt ) = @_;
2189 my %params = ( 'format' => 'template' );
2190 $params{'time'} = $today if $today;
2191 $params{'template'} = $template if $template;
2192 $params{$_} = $opt{$_}
2193 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2195 $self->print_generic( %params );
2198 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2200 Internal method - returns a filename of a filled-in LaTeX template for this
2201 invoice (Note: add ".tex" to get the actual filename), and a filename of
2202 an associated logo (with the .eps extension included).
2204 See print_ps and print_pdf for methods that return PostScript and PDF output.
2206 Options can be passed as a hashref (recommended) or as a list of time, template
2207 and then any key/value pairs for any other options.
2209 I<time>, if specified, is used to control the printing of overdue messages. The
2210 default is now. It isn't the date of the invoice; that's the `_date' field.
2211 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2212 L<Time::Local> and L<Date::Parse> for conversion functions.
2214 I<template>, if specified, is the name of a suffix for alternate invoices.
2216 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2222 my( $today, $template, %opt );
2224 %opt = %{ shift() };
2225 $today = delete($opt{'time'}) || '';
2226 $template = delete($opt{template}) || '';
2228 ( $today, $template, %opt ) = @_;
2231 my %params = ( 'format' => 'latex' );
2232 $params{'time'} = $today if $today;
2233 $params{'template'} = $template if $template;
2234 $params{$_} = $opt{$_}
2235 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2237 $template ||= $self->_agent_template;
2239 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2240 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2244 ) or die "can't open temp file: $!\n";
2246 my $agentnum = $self->cust_main->agentnum;
2248 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2249 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2250 or die "can't write temp file: $!\n";
2252 print $lh $conf->config_binary('logo.eps', $agentnum)
2253 or die "can't write temp file: $!\n";
2256 $params{'logo_file'} = $lh->filename;
2258 if($conf->exists('invoice-barcode')){
2259 my $png_file = $self->invoice_barcode($dir);
2260 my $eps_file = $png_file;
2261 $eps_file =~ s/\.png$/.eps/g;
2262 $png_file =~ /(barcode.*png)/;
2264 $eps_file =~ /(barcode.*eps)/;
2267 my $curr_dir = cwd();
2269 # after painfuly long experimentation, it was determined that sam2p won't
2270 # accept : and other chars in the path, no matter how hard I tried to
2271 # escape them, hence the chdir (and chdir back, just to be safe)
2272 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2273 or die "sam2p failed: $!\n";
2277 $params{'barcode_file'} = $eps_file;
2280 my @filled_in = $self->print_generic( %params );
2282 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2286 ) or die "can't open temp file: $!\n";
2287 print $fh join('', @filled_in );
2290 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2291 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2295 =item invoice_barcode DIR_OR_FALSE
2297 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2298 it is taken as the temp directory where the PNG file will be generated and the
2299 PNG file name is returned. Otherwise, the PNG image itself is returned.
2303 sub invoice_barcode {
2304 my ($self, $dir) = (shift,shift);
2306 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2307 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2308 my $gd = $gdbar->plot(Height => 30);
2311 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2315 ) or die "can't open temp file: $!\n";
2316 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2317 my $png_file = $bh->filename;
2324 =item print_generic OPTION => VALUE ...
2326 Internal method - returns a filled-in template for this invoice as a scalar.
2328 See print_ps and print_pdf for methods that return PostScript and PDF output.
2330 Non optional options include
2331 format - latex, html, template
2333 Optional options include
2335 template - a value used as a suffix for a configuration template
2337 time - a value used to control the printing of overdue messages. The
2338 default is now. It isn't the date of the invoice; that's the `_date' field.
2339 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2340 L<Time::Local> and L<Date::Parse> for conversion functions.
2344 unsquelch_cdr - overrides any per customer cdr squelching when true
2346 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2350 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2351 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2352 # yes: fixed width (dot matrix) text printing will be borked
2355 my( $self, %params ) = @_;
2356 my $today = $params{today} ? $params{today} : time;
2357 warn "$me print_generic called on $self with suffix $params{template}\n"
2360 my $format = $params{format};
2361 die "Unknown format: $format"
2362 unless $format =~ /^(latex|html|template)$/;
2364 my $cust_main = $self->cust_main;
2365 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2366 unless $cust_main->payname
2367 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2369 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2370 'html' => [ '<%=', '%>' ],
2371 'template' => [ '{', '}' ],
2374 warn "$me print_generic creating template\n"
2377 #create the template
2378 my $template = $params{template} ? $params{template} : $self->_agent_template;
2379 my $templatefile = "invoice_$format";
2380 $templatefile .= "_$template"
2381 if length($template) && $conf->exists($templatefile."_$template");
2382 my @invoice_template = map "$_\n", $conf->config($templatefile)
2383 or die "cannot load config data $templatefile";
2386 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2387 #change this to a die when the old code is removed
2388 warn "old-style invoice template $templatefile; ".
2389 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2390 $old_latex = 'true';
2391 @invoice_template = _translate_old_latex_format(@invoice_template);
2394 warn "$me print_generic creating T:T object\n"
2397 my $text_template = new Text::Template(
2399 SOURCE => \@invoice_template,
2400 DELIMITERS => $delimiters{$format},
2403 warn "$me print_generic compiling T:T object\n"
2406 $text_template->compile()
2407 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2410 # additional substitution could possibly cause breakage in existing templates
2411 my %convert_maps = (
2413 'notes' => sub { map "$_", @_ },
2414 'footer' => sub { map "$_", @_ },
2415 'smallfooter' => sub { map "$_", @_ },
2416 'returnaddress' => sub { map "$_", @_ },
2417 'coupon' => sub { map "$_", @_ },
2418 'summary' => sub { map "$_", @_ },
2424 s/%%(.*)$/<!-- $1 -->/g;
2425 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2426 s/\\begin\{enumerate\}/<ol>/g;
2428 s/\\end\{enumerate\}/<\/ol>/g;
2429 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2438 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2440 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2445 s/\\\\\*?\s*$/<BR>/;
2446 s/\\hyphenation\{[\w\s\-]+}//;
2451 'coupon' => sub { "" },
2452 'summary' => sub { "" },
2459 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2460 s/\\begin\{enumerate\}//g;
2462 s/\\end\{enumerate\}//g;
2463 s/\\textbf\{(.*)\}/$1/g;
2470 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2472 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2477 s/\\\\\*?\s*$/\n/; # dubious
2478 s/\\hyphenation\{[\w\s\-]+}//;
2482 'coupon' => sub { "" },
2483 'summary' => sub { "" },
2488 # hashes for differing output formats
2489 my %nbsps = ( 'latex' => '~',
2490 'html' => '', # '&nbps;' would be nice
2491 'template' => '', # not used
2493 my $nbsp = $nbsps{$format};
2495 my %escape_functions = ( 'latex' => \&_latex_escape,
2496 'html' => \&_html_escape_nbsp,#\&encode_entities,
2497 'template' => sub { shift },
2499 my $escape_function = $escape_functions{$format};
2500 my $escape_function_nonbsp = ($format eq 'html')
2501 ? \&_html_escape : $escape_function;
2503 my %date_formats = ( 'latex' => $date_format_long,
2504 'html' => $date_format_long,
2507 $date_formats{'html'} =~ s/ / /g;
2509 my $date_format = $date_formats{$format};
2511 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2513 'html' => sub { return '<b>'. shift(). '</b>'
2515 'template' => sub { shift },
2517 my $embolden_function = $embolden_functions{$format};
2519 my %newline_tokens = ( 'latex' => '\\\\',
2523 my $newline_token = $newline_tokens{$format};
2525 warn "$me generating template variables\n"
2528 # generate template variables
2531 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2535 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2541 $returnaddress = join("\n",
2542 $conf->config_orbase("invoice_${format}returnaddress", $template)
2545 } elsif ( grep /\S/,
2546 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2548 my $convert_map = $convert_maps{$format}{'returnaddress'};
2551 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2556 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2558 my $convert_map = $convert_maps{$format}{'returnaddress'};
2559 $returnaddress = join( "\n", &$convert_map(
2560 map { s/( {2,})/'~' x length($1)/eg;
2564 ( $conf->config('company_name', $self->cust_main->agentnum),
2565 $conf->config('company_address', $self->cust_main->agentnum),
2572 my $warning = "Couldn't find a return address; ".
2573 "do you need to set the company_address configuration value?";
2575 $returnaddress = $nbsp;
2576 #$returnaddress = $warning;
2580 warn "$me generating invoice data\n"
2583 my $agentnum = $self->cust_main->agentnum;
2585 my %invoice_data = (
2588 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2589 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2590 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2591 'returnaddress' => $returnaddress,
2592 'agent' => &$escape_function($cust_main->agent->agent),
2595 'invnum' => $self->invnum,
2596 'date' => time2str($date_format, $self->_date),
2597 'today' => time2str($date_format_long, $today),
2598 'terms' => $self->terms,
2599 'template' => $template, #params{'template'},
2600 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2601 'current_charges' => sprintf("%.2f", $self->charged),
2602 'duedate' => $self->due_date2str($rdate_format), #date_format?
2605 'custnum' => $cust_main->display_custnum,
2606 'agent_custid' => &$escape_function($cust_main->agent_custid),
2607 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2608 payname company address1 address2 city state zip fax
2612 'ship_enable' => $conf->exists('invoice-ship_address'),
2613 'unitprices' => $conf->exists('invoice-unitprice'),
2614 'smallernotes' => $conf->exists('invoice-smallernotes'),
2615 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2616 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2618 #layout info -- would be fancy to calc some of this and bury the template
2620 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2621 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2622 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2623 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2624 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2625 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2626 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2627 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2628 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2629 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2631 # better hang on to conf_dir for a while (for old templates)
2632 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2634 #these are only used when doing paged plaintext
2640 my $min_sdate = 999999999999;
2642 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2643 next unless $cust_bill_pkg->pkgnum > 0;
2644 $min_sdate = $cust_bill_pkg->sdate
2645 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2646 $max_edate = $cust_bill_pkg->edate
2647 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2650 $invoice_data{'bill_period'} = '';
2651 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2652 . " to " . time2str('%e %h', $max_edate)
2653 if ($max_edate != 0 && $min_sdate != 999999999999);
2655 $invoice_data{finance_section} = '';
2656 if ( $conf->config('finance_pkgclass') ) {
2658 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2659 $invoice_data{finance_section} = $pkg_class->categoryname;
2661 $invoice_data{finance_amount} = '0.00';
2662 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2664 my $countrydefault = $conf->config('countrydefault') || 'US';
2665 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2666 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2667 my $method = $prefix.$_;
2668 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2670 $invoice_data{'ship_country'} = ''
2671 if ( $invoice_data{'ship_country'} eq $countrydefault );
2673 $invoice_data{'cid'} = $params{'cid'}
2676 if ( $cust_main->country eq $countrydefault ) {
2677 $invoice_data{'country'} = '';
2679 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2683 $invoice_data{'address'} = \@address;
2685 $cust_main->payname.
2686 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2687 ? " (P.O. #". $cust_main->payinfo. ")"
2691 push @address, $cust_main->company
2692 if $cust_main->company;
2693 push @address, $cust_main->address1;
2694 push @address, $cust_main->address2
2695 if $cust_main->address2;
2697 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2698 push @address, $invoice_data{'country'}
2699 if $invoice_data{'country'};
2701 while (scalar(@address) < 5);
2703 $invoice_data{'logo_file'} = $params{'logo_file'}
2704 if $params{'logo_file'};
2705 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2706 if $params{'barcode_file'};
2707 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2708 if $params{'barcode_img'};
2709 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2710 if $params{'barcode_cid'};
2712 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2713 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2714 #my $balance_due = $self->owed + $pr_total - $cr_total;
2715 my $balance_due = $self->owed + $pr_total;
2716 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2717 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2718 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2719 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2721 my $summarypage = '';
2722 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2725 $invoice_data{'summarypage'} = $summarypage;
2727 warn "$me substituting variables in notes, footer, smallfooter\n"
2730 my @include = (qw( notes footer smallfooter ));
2731 push @include, 'coupon' unless $params{'no_coupon'};
2732 foreach my $include (@include) {
2734 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2737 if ( $conf->exists($inc_file, $agentnum)
2738 && length( $conf->config($inc_file, $agentnum) ) ) {
2740 @inc_src = $conf->config($inc_file, $agentnum);
2744 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2746 my $convert_map = $convert_maps{$format}{$include};
2748 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2749 s/--\@\]/$delimiters{$format}[1]/g;
2752 &$convert_map( $conf->config($inc_file, $agentnum) );
2756 my $inc_tt = new Text::Template (
2758 SOURCE => [ map "$_\n", @inc_src ],
2759 DELIMITERS => $delimiters{$format},
2760 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2762 unless ( $inc_tt->compile() ) {
2763 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2764 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2768 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2770 $invoice_data{$include} =~ s/\n+$//
2771 if ($format eq 'latex');
2774 $invoice_data{'po_line'} =
2775 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2776 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2779 my %money_chars = ( 'latex' => '',
2780 'html' => $conf->config('money_char') || '$',
2783 my $money_char = $money_chars{$format};
2785 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2786 'html' => $conf->config('money_char') || '$',
2789 my $other_money_char = $other_money_chars{$format};
2790 $invoice_data{'dollar'} = $other_money_char;
2792 my @detail_items = ();
2793 my @total_items = ();
2797 $invoice_data{'detail_items'} = \@detail_items;
2798 $invoice_data{'total_items'} = \@total_items;
2799 $invoice_data{'buf'} = \@buf;
2800 $invoice_data{'sections'} = \@sections;
2802 warn "$me generating sections\n"
2805 my $previous_section = { 'description' => 'Previous Charges',
2806 'subtotal' => $other_money_char.
2807 sprintf('%.2f', $pr_total),
2808 'summarized' => $summarypage ? 'Y' : '',
2810 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2811 join(' / ', map { $cust_main->balance_date_range(@$_) }
2812 $self->_prior_month30s
2814 if $conf->exists('invoice_include_aging');
2817 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2818 'subtotal' => $taxtotal, # adjusted below
2819 'summarized' => $summarypage ? 'Y' : '',
2821 my $tax_weight = _pkg_category($tax_section->{description})
2822 ? _pkg_category($tax_section->{description})->weight
2824 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2825 $tax_section->{'sort_weight'} = $tax_weight;
2828 my $adjusttotal = 0;
2829 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2830 'subtotal' => 0, # adjusted below
2831 'summarized' => $summarypage ? 'Y' : '',
2833 my $adjust_weight = _pkg_category($adjust_section->{description})
2834 ? _pkg_category($adjust_section->{description})->weight
2836 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2837 $adjust_section->{'sort_weight'} = $adjust_weight;
2839 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2840 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2841 $invoice_data{'multisection'} = $multisection;
2842 my $late_sections = [];
2843 my $extra_sections = [];
2844 my $extra_lines = ();
2845 if ( $multisection ) {
2846 ($extra_sections, $extra_lines) =
2847 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2848 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2850 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2852 push @detail_items, @$extra_lines if $extra_lines;
2854 $self->_items_sections( $late_sections, # this could stand a refactor
2856 $escape_function_nonbsp,
2860 if ($conf->exists('svc_phone_sections')) {
2861 my ($phone_sections, $phone_lines) =
2862 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2863 push @{$late_sections}, @$phone_sections;
2864 push @detail_items, @$phone_lines;
2866 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2867 my ($accountcode_section, $accountcode_lines) =
2868 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2869 if ( scalar(@$accountcode_lines) ) {
2870 push @{$late_sections}, $accountcode_section;
2871 push @detail_items, @$accountcode_lines;
2875 push @sections, { 'description' => '', 'subtotal' => '' };
2878 unless ( $conf->exists('disable_previous_balance')
2879 || $conf->exists('previous_balance-summary_only')
2883 warn "$me adding previous balances\n"
2886 foreach my $line_item ( $self->_items_previous ) {
2889 ext_description => [],
2891 $detail->{'ref'} = $line_item->{'pkgnum'};
2892 $detail->{'quantity'} = 1;
2893 $detail->{'section'} = $previous_section;
2894 $detail->{'description'} = &$escape_function($line_item->{'description'});
2895 if ( exists $line_item->{'ext_description'} ) {
2896 @{$detail->{'ext_description'}} = map {
2897 &$escape_function($_);
2898 } @{$line_item->{'ext_description'}};
2900 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2901 $line_item->{'amount'};
2902 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2904 push @detail_items, $detail;
2905 push @buf, [ $detail->{'description'},
2906 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2912 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2913 push @buf, ['','-----------'];
2914 push @buf, [ 'Total Previous Balance',
2915 $money_char. sprintf("%10.2f", $pr_total) ];
2919 if ( $conf->exists('svc_phone-did-summary') ) {
2920 warn "$me adding DID summary\n"
2923 my ($didsummary,$minutes) = $self->_did_summary;
2924 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2926 { 'description' => $didsummary_desc,
2927 'ext_description' => [ $didsummary, $minutes ],
2931 foreach my $section (@sections, @$late_sections) {
2933 warn "$me adding section \n". Dumper($section)
2936 # begin some normalization
2937 $section->{'subtotal'} = $section->{'amount'}
2939 && !exists($section->{subtotal})
2940 && exists($section->{amount});
2942 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2943 if ( $invoice_data{finance_section} &&
2944 $section->{'description'} eq $invoice_data{finance_section} );
2946 $section->{'subtotal'} = $other_money_char.
2947 sprintf('%.2f', $section->{'subtotal'})
2950 # continue some normalization
2951 $section->{'amount'} = $section->{'subtotal'}
2955 if ( $section->{'description'} ) {
2956 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2961 warn "$me setting options\n"
2964 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2966 $options{'section'} = $section if $multisection;
2967 $options{'format'} = $format;
2968 $options{'escape_function'} = $escape_function;
2969 $options{'format_function'} = sub { () } unless $unsquelched;
2970 $options{'unsquelched'} = $unsquelched;
2971 $options{'summary_page'} = $summarypage;
2972 $options{'skip_usage'} =
2973 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2974 $options{'multilocation'} = $multilocation;
2975 $options{'multisection'} = $multisection;
2977 warn "$me searching for line items\n"
2980 foreach my $line_item ( $self->_items_pkg(%options) ) {
2982 warn "$me adding line item $line_item\n"
2986 ext_description => [],
2988 $detail->{'ref'} = $line_item->{'pkgnum'};
2989 $detail->{'quantity'} = $line_item->{'quantity'};
2990 $detail->{'section'} = $section;
2991 $detail->{'description'} = &$escape_function($line_item->{'description'});
2992 if ( exists $line_item->{'ext_description'} ) {
2993 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2995 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2996 $line_item->{'amount'};
2997 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2998 $line_item->{'unit_amount'};
2999 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3001 push @detail_items, $detail;
3002 push @buf, ( [ $detail->{'description'},
3003 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3005 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3009 if ( $section->{'description'} ) {
3010 push @buf, ( ['','-----------'],
3011 [ $section->{'description'}. ' sub-total',
3012 $money_char. sprintf("%10.2f", $section->{'subtotal'})
3021 $invoice_data{current_less_finance} =
3022 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3024 if ( $multisection && !$conf->exists('disable_previous_balance')
3025 || $conf->exists('previous_balance-summary_only') )
3027 unshift @sections, $previous_section if $pr_total;
3030 warn "$me adding taxes\n"
3033 foreach my $tax ( $self->_items_tax ) {
3035 $taxtotal += $tax->{'amount'};
3037 my $description = &$escape_function( $tax->{'description'} );
3038 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3040 if ( $multisection ) {
3042 my $money = $old_latex ? '' : $money_char;
3043 push @detail_items, {
3044 ext_description => [],
3047 description => $description,
3048 amount => $money. $amount,
3050 section => $tax_section,
3055 push @total_items, {
3056 'total_item' => $description,
3057 'total_amount' => $other_money_char. $amount,
3062 push @buf,[ $description,
3063 $money_char. $amount,
3070 $total->{'total_item'} = 'Sub-total';
3071 $total->{'total_amount'} =
3072 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3074 if ( $multisection ) {
3075 $tax_section->{'subtotal'} = $other_money_char.
3076 sprintf('%.2f', $taxtotal);
3077 $tax_section->{'pretotal'} = 'New charges sub-total '.
3078 $total->{'total_amount'};
3079 push @sections, $tax_section if $taxtotal;
3081 unshift @total_items, $total;
3084 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3086 push @buf,['','-----------'];
3087 push @buf,[( $conf->exists('disable_previous_balance')
3089 : 'Total New Charges'
3091 $money_char. sprintf("%10.2f",$self->charged) ];
3097 $item = $conf->config('previous_balance-exclude_from_total')
3098 || 'Total New Charges'
3099 if $conf->exists('previous_balance-exclude_from_total');
3100 my $amount = $self->charged +
3101 ( $conf->exists('disable_previous_balance') ||
3102 $conf->exists('previous_balance-exclude_from_total')
3106 $total->{'total_item'} = &$embolden_function($item);
3107 $total->{'total_amount'} =
3108 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3109 if ( $multisection ) {
3110 if ( $adjust_section->{'sort_weight'} ) {
3111 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3112 sprintf("%.2f", ($self->billing_balance || 0) );
3114 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3115 sprintf('%.2f', $self->charged );
3118 push @total_items, $total;
3120 push @buf,['','-----------'];
3123 sprintf( '%10.2f', $amount )
3128 unless ( $conf->exists('disable_previous_balance') ) {
3129 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3132 my $credittotal = 0;
3133 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3136 $total->{'total_item'} = &$escape_function($credit->{'description'});
3137 $credittotal += $credit->{'amount'};
3138 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3139 $adjusttotal += $credit->{'amount'};
3140 if ( $multisection ) {
3141 my $money = $old_latex ? '' : $money_char;
3142 push @detail_items, {
3143 ext_description => [],
3146 description => &$escape_function($credit->{'description'}),
3147 amount => $money. $credit->{'amount'},
3149 section => $adjust_section,
3152 push @total_items, $total;
3156 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3159 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3160 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3164 my $paymenttotal = 0;
3165 foreach my $payment ( $self->_items_payments ) {
3167 $total->{'total_item'} = &$escape_function($payment->{'description'});
3168 $paymenttotal += $payment->{'amount'};
3169 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3170 $adjusttotal += $payment->{'amount'};
3171 if ( $multisection ) {
3172 my $money = $old_latex ? '' : $money_char;
3173 push @detail_items, {
3174 ext_description => [],
3177 description => &$escape_function($payment->{'description'}),
3178 amount => $money. $payment->{'amount'},
3180 section => $adjust_section,
3183 push @total_items, $total;
3185 push @buf, [ $payment->{'description'},
3186 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3189 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3191 if ( $multisection ) {
3192 $adjust_section->{'subtotal'} = $other_money_char.
3193 sprintf('%.2f', $adjusttotal);
3194 push @sections, $adjust_section
3195 unless $adjust_section->{sort_weight};
3200 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3201 $total->{'total_amount'} =
3202 &$embolden_function(
3203 $other_money_char. sprintf('%.2f', $summarypage
3205 $self->billing_balance
3206 : $self->owed + $pr_total
3209 if ( $multisection && !$adjust_section->{sort_weight} ) {
3210 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3211 $total->{'total_amount'};
3213 push @total_items, $total;
3215 push @buf,['','-----------'];
3216 push @buf,[$self->balance_due_msg, $money_char.
3217 sprintf("%10.2f", $balance_due ) ];
3220 if ( $conf->exists('previous_balance-show_credit')
3221 and $cust_main->balance < 0 ) {
3222 my $credit_total = {
3223 'total_item' => &$embolden_function($self->credit_balance_msg),
3224 'total_amount' => &$embolden_function(
3225 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3228 if ( $multisection ) {
3229 $adjust_section->{'posttotal'} .= $newline_token .
3230 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3233 push @total_items, $credit_total;
3235 push @buf,['','-----------'];
3236 push @buf,[$self->credit_balance_msg, $money_char.
3237 sprintf("%10.2f", -$cust_main->balance ) ];
3241 if ( $multisection ) {
3242 if ($conf->exists('svc_phone_sections')) {
3244 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3245 $total->{'total_amount'} =
3246 &$embolden_function(
3247 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3249 my $last_section = pop @sections;
3250 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3251 $total->{'total_amount'};
3252 push @sections, $last_section;
3254 push @sections, @$late_sections
3258 my @includelist = ();
3259 push @includelist, 'summary' if $summarypage;
3260 foreach my $include ( @includelist ) {
3262 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3265 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3267 @inc_src = $conf->config($inc_file, $agentnum);
3271 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3273 my $convert_map = $convert_maps{$format}{$include};
3275 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3276 s/--\@\]/$delimiters{$format}[1]/g;
3279 &$convert_map( $conf->config($inc_file, $agentnum) );
3283 my $inc_tt = new Text::Template (
3285 SOURCE => [ map "$_\n", @inc_src ],
3286 DELIMITERS => $delimiters{$format},
3287 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3289 unless ( $inc_tt->compile() ) {
3290 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3291 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3295 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3297 $invoice_data{$include} =~ s/\n+$//
3298 if ($format eq 'latex');
3303 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3304 /invoice_lines\((\d*)\)/;
3305 $invoice_lines += $1 || scalar(@buf);
3308 die "no invoice_lines() functions in template?"
3309 if ( $format eq 'template' && !$wasfunc );
3311 if ($format eq 'template') {
3313 if ( $invoice_lines ) {
3314 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3315 $invoice_data{'total_pages'}++
3316 if scalar(@buf) % $invoice_lines;
3319 #setup subroutine for the template
3320 sub FS::cust_bill::_template::invoice_lines {
3321 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3323 scalar(@FS::cust_bill::_template::buf)
3324 ? shift @FS::cust_bill::_template::buf
3333 push @collect, split("\n",
3334 $text_template->fill_in( HASH => \%invoice_data,
3335 PACKAGE => 'FS::cust_bill::_template'
3338 $FS::cust_bill::_template::page++;
3340 map "$_\n", @collect;
3342 warn "filling in template for invoice ". $self->invnum. "\n"
3344 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3347 $text_template->fill_in(HASH => \%invoice_data);
3351 # helper routine for generating date ranges
3352 sub _prior_month30s {
3355 [ 1, 2592000 ], # 0-30 days ago
3356 [ 2592000, 5184000 ], # 30-60 days ago
3357 [ 5184000, 7776000 ], # 60-90 days ago
3358 [ 7776000, 0 ], # 90+ days ago
3361 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3362 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3367 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3369 Returns an postscript invoice, as a scalar.
3371 Options can be passed as a hashref (recommended) or as a list of time, template
3372 and then any key/value pairs for any other options.
3374 I<time> an optional value used to control the printing of overdue messages. The
3375 default is now. It isn't the date of the invoice; that's the `_date' field.
3376 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3377 L<Time::Local> and L<Date::Parse> for conversion functions.
3379 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3386 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3387 my $ps = generate_ps($file);
3389 unlink($barcodefile) if $barcodefile;
3394 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3396 Returns an PDF invoice, as a scalar.
3398 Options can be passed as a hashref (recommended) or as a list of time, template
3399 and then any key/value pairs for any other options.
3401 I<time> an optional value used to control the printing of overdue messages. The
3402 default is now. It isn't the date of the invoice; that's the `_date' field.
3403 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3404 L<Time::Local> and L<Date::Parse> for conversion functions.
3406 I<template>, if specified, is the name of a suffix for alternate invoices.
3408 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3415 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3416 my $pdf = generate_pdf($file);
3418 unlink($barcodefile) if $barcodefile;
3423 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3425 Returns an HTML invoice, as a scalar.
3427 I<time> an optional value used to control the printing of overdue messages. The
3428 default is now. It isn't the date of the invoice; that's the `_date' field.
3429 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3430 L<Time::Local> and L<Date::Parse> for conversion functions.
3432 I<template>, if specified, is the name of a suffix for alternate invoices.
3434 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3436 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3437 when emailing the invoice as part of a multipart/related MIME email.
3445 %params = %{ shift() };
3447 $params{'time'} = shift;
3448 $params{'template'} = shift;
3449 $params{'cid'} = shift;
3452 $params{'format'} = 'html';
3454 $self->print_generic( %params );
3457 # quick subroutine for print_latex
3459 # There are ten characters that LaTeX treats as special characters, which
3460 # means that they do not simply typeset themselves:
3461 # # $ % & ~ _ ^ \ { }
3463 # TeX ignores blanks following an escaped character; if you want a blank (as
3464 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3468 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3469 $value =~ s/([<>])/\$$1\$/g;
3475 encode_entities($value);
3479 sub _html_escape_nbsp {
3480 my $value = _html_escape(shift);
3481 $value =~ s/ +/ /g;
3485 #utility methods for print_*
3487 sub _translate_old_latex_format {
3488 warn "_translate_old_latex_format called\n"
3495 if ( $line =~ /^%%Detail\s*$/ ) {
3497 push @template, q![@--!,
3498 q! foreach my $_tr_line (@detail_items) {!,
3499 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3500 q! $_tr_line->{'description'} .= !,
3501 q! "\\tabularnewline\n~~".!,
3502 q! join( "\\tabularnewline\n~~",!,
3503 q! @{$_tr_line->{'ext_description'}}!,
3507 while ( ( my $line_item_line = shift )
3508 !~ /^%%EndDetail\s*$/ ) {
3509 $line_item_line =~ s/'/\\'/g; # nice LTS
3510 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3511 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3512 push @template, " \$OUT .= '$line_item_line';";
3515 push @template, '}',
3518 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3520 push @template, '[@--',
3521 ' foreach my $_tr_line (@total_items) {';
3523 while ( ( my $total_item_line = shift )
3524 !~ /^%%EndTotalDetails\s*$/ ) {
3525 $total_item_line =~ s/'/\\'/g; # nice LTS
3526 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3527 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3528 push @template, " \$OUT .= '$total_item_line';";
3531 push @template, '}',
3535 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3536 push @template, $line;
3542 warn "$_\n" foreach @template;
3551 #check for an invoice-specific override
3552 return $self->invoice_terms if $self->invoice_terms;
3554 #check for a customer- specific override
3555 my $cust_main = $self->cust_main;
3556 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3558 #use configured default
3559 $conf->config('invoice_default_terms') || '';
3565 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3566 $duedate = $self->_date() + ( $1 * 86400 );
3573 $self->due_date ? time2str(shift, $self->due_date) : '';
3576 sub balance_due_msg {
3578 my $msg = 'Balance Due';
3579 return $msg unless $self->terms;
3580 if ( $self->due_date ) {
3581 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3582 } elsif ( $self->terms ) {
3583 $msg .= ' - '. $self->terms;
3588 sub balance_due_date {
3591 if ( $conf->exists('invoice_default_terms')
3592 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3593 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3598 sub credit_balance_msg { 'Credit Balance Remaining' }
3600 =item invnum_date_pretty
3602 Returns a string with the invoice number and date, for example:
3603 "Invoice #54 (3/20/2008)"
3607 sub invnum_date_pretty {
3609 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3614 Returns a string with the date, for example: "3/20/2008"
3620 time2str($date_format, $self->_date);
3623 use vars qw(%pkg_category_cache);
3624 sub _items_sections {
3627 my $summarypage = shift;
3629 my $extra_sections = shift;
3633 my %late_subtotal = ();
3636 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3639 my $usage = $cust_bill_pkg->usage;
3641 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3642 next if ( $display->summary && $summarypage );
3644 my $section = $display->section;
3645 my $type = $display->type;
3647 $not_tax{$section} = 1
3648 unless $cust_bill_pkg->pkgnum == 0;
3650 if ( $display->post_total && !$summarypage ) {
3651 if (! $type || $type eq 'S') {
3652 $late_subtotal{$section} += $cust_bill_pkg->setup
3653 if $cust_bill_pkg->setup != 0;
3657 $late_subtotal{$section} += $cust_bill_pkg->recur
3658 if $cust_bill_pkg->recur != 0;
3661 if ($type && $type eq 'R') {
3662 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3663 if $cust_bill_pkg->recur != 0;
3666 if ($type && $type eq 'U') {
3667 $late_subtotal{$section} += $usage
3668 unless scalar(@$extra_sections);
3673 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3675 if (! $type || $type eq 'S') {
3676 $subtotal{$section} += $cust_bill_pkg->setup
3677 if $cust_bill_pkg->setup != 0;
3681 $subtotal{$section} += $cust_bill_pkg->recur
3682 if $cust_bill_pkg->recur != 0;
3685 if ($type && $type eq 'R') {
3686 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3687 if $cust_bill_pkg->recur != 0;
3690 if ($type && $type eq 'U') {
3691 $subtotal{$section} += $usage
3692 unless scalar(@$extra_sections);
3701 %pkg_category_cache = ();
3703 push @$late, map { { 'description' => &{$escape}($_),
3704 'subtotal' => $late_subtotal{$_},
3706 'sort_weight' => ( _pkg_category($_)
3707 ? _pkg_category($_)->weight
3710 ((_pkg_category($_) && _pkg_category($_)->condense)
3711 ? $self->_condense_section($format)
3715 sort _sectionsort keys %late_subtotal;
3718 if ( $summarypage ) {
3719 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3720 map { $_->categoryname } qsearch('pkg_category', {});
3721 push @sections, '' if exists($subtotal{''});
3723 @sections = keys %subtotal;
3726 my @early = map { { 'description' => &{$escape}($_),
3727 'subtotal' => $subtotal{$_},
3728 'summarized' => $not_tax{$_} ? '' : 'Y',
3729 'tax_section' => $not_tax{$_} ? '' : 'Y',
3730 'sort_weight' => ( _pkg_category($_)
3731 ? _pkg_category($_)->weight
3734 ((_pkg_category($_) && _pkg_category($_)->condense)
3735 ? $self->_condense_section($format)
3740 push @early, @$extra_sections if $extra_sections;
3742 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3746 #helper subs for above
3749 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3753 my $categoryname = shift;
3754 $pkg_category_cache{$categoryname} ||=
3755 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3758 my %condensed_format = (
3759 'label' => [ qw( Description Qty Amount ) ],
3761 sub { shift->{description} },
3762 sub { shift->{quantity} },
3763 sub { my($href, %opt) = @_;
3764 ($opt{dollar} || ''). $href->{amount};
3767 'align' => [ qw( l r r ) ],
3768 'span' => [ qw( 5 1 1 ) ], # unitprices?
3769 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3772 sub _condense_section {
3773 my ( $self, $format ) = ( shift, shift );
3775 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3776 qw( description_generator
3779 total_line_generator
3784 sub _condensed_generator_defaults {
3785 my ( $self, $format ) = ( shift, shift );
3786 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3795 sub _condensed_header_generator {
3796 my ( $self, $format ) = ( shift, shift );
3798 my ( $f, $prefix, $suffix, $separator, $column ) =
3799 _condensed_generator_defaults($format);
3801 if ($format eq 'latex') {
3802 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3803 $suffix = "\\\\\n\\hline";
3806 sub { my ($d,$a,$s,$w) = @_;
3807 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3809 } elsif ( $format eq 'html' ) {
3810 $prefix = '<th></th>';
3814 sub { my ($d,$a,$s,$w) = @_;
3815 return qq!<th align="$html_align{$a}">$d</th>!;
3823 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3825 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3828 $prefix. join($separator, @result). $suffix;
3833 sub _condensed_description_generator {
3834 my ( $self, $format ) = ( shift, shift );
3836 my ( $f, $prefix, $suffix, $separator, $column ) =
3837 _condensed_generator_defaults($format);
3839 my $money_char = '$';
3840 if ($format eq 'latex') {
3841 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3843 $separator = " & \n";
3845 sub { my ($d,$a,$s,$w) = @_;
3846 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3848 $money_char = '\\dollar';
3849 }elsif ( $format eq 'html' ) {
3850 $prefix = '"><td align="center"></td>';
3854 sub { my ($d,$a,$s,$w) = @_;
3855 return qq!<td align="$html_align{$a}">$d</td>!;
3857 #$money_char = $conf->config('money_char') || '$';
3858 $money_char = ''; # this is madness
3866 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3868 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3870 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3871 map { $f->{$_}->[$i] } qw(align span width)
3875 $prefix. join( $separator, @result ). $suffix;
3880 sub _condensed_total_generator {
3881 my ( $self, $format ) = ( shift, shift );
3883 my ( $f, $prefix, $suffix, $separator, $column ) =
3884 _condensed_generator_defaults($format);
3887 if ($format eq 'latex') {
3890 $separator = " & \n";
3892 sub { my ($d,$a,$s,$w) = @_;
3893 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3895 }elsif ( $format eq 'html' ) {
3899 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3901 sub { my ($d,$a,$s,$w) = @_;
3902 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3911 # my $r = &{$f->{fields}->[$i]}(@args);
3912 # $r .= ' Total' unless $i;
3914 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3916 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3917 map { $f->{$_}->[$i] } qw(align span width)
3921 $prefix. join( $separator, @result ). $suffix;
3926 =item total_line_generator FORMAT
3928 Returns a coderef used for generation of invoice total line items for this
3929 usage_class. FORMAT is either html or latex
3933 # should not be used: will have issues with hash element names (description vs
3934 # total_item and amount vs total_amount -- another array of functions?
3936 sub _condensed_total_line_generator {
3937 my ( $self, $format ) = ( shift, shift );
3939 my ( $f, $prefix, $suffix, $separator, $column ) =
3940 _condensed_generator_defaults($format);
3943 if ($format eq 'latex') {
3946 $separator = " & \n";
3948 sub { my ($d,$a,$s,$w) = @_;
3949 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3951 }elsif ( $format eq 'html' ) {
3955 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3957 sub { my ($d,$a,$s,$w) = @_;
3958 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3967 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3969 &{$column}( &{$f->{fields}->[$i]}(@args),
3970 map { $f->{$_}->[$i] } qw(align span width)
3974 $prefix. join( $separator, @result ). $suffix;
3979 #sub _items_extra_usage_sections {
3981 # my $escape = shift;
3983 # my %sections = ();
3985 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3986 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3988 # next unless $cust_bill_pkg->pkgnum > 0;
3990 # foreach my $section ( keys %usage_class ) {
3992 # my $usage = $cust_bill_pkg->usage($section);
3994 # next unless $usage && $usage > 0;
3996 # $sections{$section} ||= 0;
3997 # $sections{$section} += $usage;
4003 # map { { 'description' => &{$escape}($_),
4004 # 'subtotal' => $sections{$_},
4005 # 'summarized' => '',
4006 # 'tax_section' => '',
4009 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4013 sub _items_extra_usage_sections {
4022 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4024 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4025 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4026 next unless $cust_bill_pkg->pkgnum > 0;
4028 foreach my $classnum ( keys %usage_class ) {
4029 my $section = $usage_class{$classnum}->classname;
4030 $classnums{$section} = $classnum;
4032 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4033 my $amount = $detail->amount;
4034 next unless $amount && $amount > 0;
4036 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4037 $sections{$section}{amount} += $amount; #subtotal
4038 $sections{$section}{calls}++;
4039 $sections{$section}{duration} += $detail->duration;
4041 my $desc = $detail->regionname;
4042 my $description = $desc;
4043 $description = substr($desc, 0, $maxlength). '...'
4044 if $format eq 'latex' && length($desc) > $maxlength;
4046 $lines{$section}{$desc} ||= {
4047 description => &{$escape}($description),
4048 #pkgpart => $part_pkg->pkgpart,
4049 pkgnum => $cust_bill_pkg->pkgnum,
4054 #unit_amount => $cust_bill_pkg->unitrecur,
4055 quantity => $cust_bill_pkg->quantity,
4056 product_code => 'N/A',
4057 ext_description => [],
4060 $lines{$section}{$desc}{amount} += $amount;
4061 $lines{$section}{$desc}{calls}++;
4062 $lines{$section}{$desc}{duration} += $detail->duration;
4068 my %sectionmap = ();
4069 foreach (keys %sections) {
4070 my $usage_class = $usage_class{$classnums{$_}};
4071 $sectionmap{$_} = { 'description' => &{$escape}($_),
4072 'amount' => $sections{$_}{amount}, #subtotal
4073 'calls' => $sections{$_}{calls},
4074 'duration' => $sections{$_}{duration},
4076 'tax_section' => '',
4077 'sort_weight' => $usage_class->weight,
4078 ( $usage_class->format
4079 ? ( map { $_ => $usage_class->$_($format) }
4080 qw( description_generator header_generator total_generator total_line_generator )
4087 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4091 foreach my $section ( keys %lines ) {
4092 foreach my $line ( keys %{$lines{$section}} ) {
4093 my $l = $lines{$section}{$line};
4094 $l->{section} = $sectionmap{$section};
4095 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4096 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4101 return(\@sections, \@lines);
4107 my $end = $self->_date;
4109 # start at date of previous invoice + 1 second or 0 if no previous invoice
4110 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4111 $start = 0 if !$start;
4114 my $cust_main = $self->cust_main;
4115 my @pkgs = $cust_main->all_pkgs;
4116 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4119 foreach my $pkg ( @pkgs ) {
4120 my @h_cust_svc = $pkg->h_cust_svc($end);
4121 foreach my $h_cust_svc ( @h_cust_svc ) {
4122 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4123 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4125 my $inserted = $h_cust_svc->date_inserted;
4126 my $deleted = $h_cust_svc->date_deleted;
4127 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4129 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4131 # DID either activated or ported in; cannot be both for same DID simultaneously
4132 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4133 && (!$phone_inserted->lnp_status
4134 || $phone_inserted->lnp_status eq ''
4135 || $phone_inserted->lnp_status eq 'native')) {
4138 else { # this one not so clean, should probably move to (h_)svc_phone
4139 my $phone_portedin = qsearchs( 'h_svc_phone',
4140 { 'svcnum' => $h_cust_svc->svcnum,
4141 'lnp_status' => 'portedin' },
4142 FS::h_svc_phone->sql_h_searchs($end),
4144 $num_portedin++ if $phone_portedin;
4147 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4148 if($deleted >= $start && $deleted <= $end && $phone_deleted
4149 && (!$phone_deleted->lnp_status
4150 || $phone_deleted->lnp_status ne 'portingout')) {
4153 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4154 && $phone_deleted->lnp_status
4155 && $phone_deleted->lnp_status eq 'portingout') {
4159 # increment usage minutes
4160 if ( $phone_inserted ) {
4161 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4162 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4165 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4168 # don't look at this service again
4169 push @seen, $h_cust_svc->svcnum;
4173 $minutes = sprintf("%d", $minutes);
4174 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4175 . "$num_deactivated Ported-Out: $num_portedout ",
4176 "Total Minutes: $minutes");
4179 sub _items_accountcode_cdr {
4184 my $section = { 'amount' => 0,
4187 'sort_weight' => '',
4189 'description' => 'Usage by Account Code',
4195 my %accountcodes = ();
4197 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4198 next unless $cust_bill_pkg->pkgnum > 0;
4200 my @header = $cust_bill_pkg->details_header;
4201 next unless scalar(@header);
4202 $section->{'header'} = join(',',@header);
4204 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4206 $section->{'header'} = $detail->formatted('format' => $format)
4207 if($detail->detail eq $section->{'header'});
4209 my $accountcode = $detail->accountcode;
4210 next unless $accountcode;
4212 my $amount = $detail->amount;
4213 next unless $amount && $amount > 0;
4215 $accountcodes{$accountcode} ||= {
4216 description => $accountcode,
4223 product_code => 'N/A',
4224 section => $section,
4225 ext_description => [],
4228 $section->{'amount'} += $amount;
4229 $accountcodes{$accountcode}{'amount'} += $amount;
4230 $accountcodes{$accountcode}{calls}++;
4231 $accountcodes{$accountcode}{duration} += $detail->duration;
4232 push @{$accountcodes{$accountcode}{ext_description}},
4233 $detail->formatted('format' => $format);
4237 foreach my $l ( values %accountcodes ) {
4238 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4239 unshift @{$l->{ext_description}}, $section->{'header'};
4243 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4245 return ($section,\@sorted_lines);
4248 sub _items_svc_phone_sections {
4257 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4259 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4260 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4262 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4263 next unless $cust_bill_pkg->pkgnum > 0;
4265 my @header = $cust_bill_pkg->details_header;
4266 next unless scalar(@header);
4268 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4270 my $phonenum = $detail->phonenum;
4271 next unless $phonenum;
4273 my $amount = $detail->amount;
4274 next unless $amount && $amount > 0;
4276 $sections{$phonenum} ||= { 'amount' => 0,
4279 'sort_weight' => -1,
4280 'phonenum' => $phonenum,
4282 $sections{$phonenum}{amount} += $amount; #subtotal
4283 $sections{$phonenum}{calls}++;
4284 $sections{$phonenum}{duration} += $detail->duration;
4286 my $desc = $detail->regionname;
4287 my $description = $desc;
4288 $description = substr($desc, 0, $maxlength). '...'
4289 if $format eq 'latex' && length($desc) > $maxlength;
4291 $lines{$phonenum}{$desc} ||= {
4292 description => &{$escape}($description),
4293 #pkgpart => $part_pkg->pkgpart,
4301 product_code => 'N/A',
4302 ext_description => [],
4305 $lines{$phonenum}{$desc}{amount} += $amount;
4306 $lines{$phonenum}{$desc}{calls}++;
4307 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4309 my $line = $usage_class{$detail->classnum}->classname;
4310 $sections{"$phonenum $line"} ||=
4314 'sort_weight' => $usage_class{$detail->classnum}->weight,
4315 'phonenum' => $phonenum,
4316 'header' => [ @header ],
4318 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4319 $sections{"$phonenum $line"}{calls}++;
4320 $sections{"$phonenum $line"}{duration} += $detail->duration;
4322 $lines{"$phonenum $line"}{$desc} ||= {
4323 description => &{$escape}($description),
4324 #pkgpart => $part_pkg->pkgpart,
4332 product_code => 'N/A',
4333 ext_description => [],
4336 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4337 $lines{"$phonenum $line"}{$desc}{calls}++;
4338 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4339 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4340 $detail->formatted('format' => $format);
4345 my %sectionmap = ();
4346 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4347 foreach ( keys %sections ) {
4348 my @header = @{ $sections{$_}{header} || [] };
4350 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4351 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4352 my $usage_class = $summary ? $simple : $usage_simple;
4353 my $ending = $summary ? ' usage charges' : '';
4356 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4358 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4359 'amount' => $sections{$_}{amount}, #subtotal
4360 'calls' => $sections{$_}{calls},
4361 'duration' => $sections{$_}{duration},
4363 'tax_section' => '',
4364 'phonenum' => $sections{$_}{phonenum},
4365 'sort_weight' => $sections{$_}{sort_weight},
4366 'post_total' => $summary, #inspire pagebreak
4368 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4369 qw( description_generator
4372 total_line_generator
4379 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4380 $a->{sort_weight} <=> $b->{sort_weight}
4385 foreach my $section ( keys %lines ) {
4386 foreach my $line ( keys %{$lines{$section}} ) {
4387 my $l = $lines{$section}{$line};
4388 $l->{section} = $sectionmap{$section};
4389 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4390 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4395 if($conf->exists('phone_usage_class_summary')) {
4396 # this only works with Latex
4400 # after this, we'll have only two sections per DID:
4401 # Calls Summary and Calls Detail
4402 foreach my $section ( @sections ) {
4403 if($section->{'post_total'}) {
4404 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4405 $section->{'total_line_generator'} = sub { '' };
4406 $section->{'total_generator'} = sub { '' };
4407 $section->{'header_generator'} = sub { '' };
4408 $section->{'description_generator'} = '';
4409 push @newsections, $section;
4410 my %calls_detail = %$section;
4411 $calls_detail{'post_total'} = '';
4412 $calls_detail{'sort_weight'} = '';
4413 $calls_detail{'description_generator'} = sub { '' };
4414 $calls_detail{'header_generator'} = sub {
4415 return ' & Date/Time & Called Number & Duration & Price'
4416 if $format eq 'latex';
4419 $calls_detail{'description'} = 'Calls Detail: '
4420 . $section->{'phonenum'};
4421 push @newsections, \%calls_detail;
4425 # after this, each usage class is collapsed/summarized into a single
4426 # line under the Calls Summary section
4427 foreach my $newsection ( @newsections ) {
4428 if($newsection->{'post_total'}) { # this means Calls Summary
4429 foreach my $section ( @sections ) {
4430 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4431 && !$section->{'post_total'});
4432 my $newdesc = $section->{'description'};
4433 my $tn = $section->{'phonenum'};
4434 $newdesc =~ s/$tn//g;
4435 my $line = { ext_description => [],
4439 calls => $section->{'calls'},
4440 section => $newsection,
4441 duration => $section->{'duration'},
4442 description => $newdesc,
4443 amount => sprintf("%.2f",$section->{'amount'}),
4444 product_code => 'N/A',
4446 push @newlines, $line;
4451 # after this, Calls Details is populated with all CDRs
4452 foreach my $newsection ( @newsections ) {
4453 if(!$newsection->{'post_total'}) { # this means Calls Details
4454 foreach my $line ( @lines ) {
4455 next unless (scalar(@{$line->{'ext_description'}}) &&
4456 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4458 my @extdesc = @{$line->{'ext_description'}};
4460 foreach my $extdesc ( @extdesc ) {
4461 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4462 push @newextdesc, $extdesc;
4464 $line->{'ext_description'} = \@newextdesc;
4465 $line->{'section'} = $newsection;
4466 push @newlines, $line;
4471 return(\@newsections, \@newlines);
4474 return(\@sections, \@lines);
4481 #my @display = scalar(@_)
4483 # : qw( _items_previous _items_pkg );
4484 # #: qw( _items_pkg );
4485 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4486 my @display = qw( _items_previous _items_pkg );
4489 foreach my $display ( @display ) {
4490 push @b, $self->$display(@_);
4495 sub _items_previous {
4497 my $cust_main = $self->cust_main;
4498 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4500 foreach ( @pr_cust_bill ) {
4501 my $date = $conf->exists('invoice_show_prior_due_date')
4502 ? 'due '. $_->due_date2str($date_format)
4503 : time2str($date_format, $_->_date);
4505 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4506 #'pkgpart' => 'N/A',
4508 'amount' => sprintf("%.2f", $_->owed),
4514 # 'description' => 'Previous Balance',
4515 # #'pkgpart' => 'N/A',
4516 # 'pkgnum' => 'N/A',
4517 # 'amount' => sprintf("%10.2f", $pr_total ),
4518 # 'ext_description' => [ map {
4519 # "Invoice ". $_->invnum.
4520 # " (". time2str("%x",$_->_date). ") ".
4521 # sprintf("%10.2f", $_->owed)
4522 # } @pr_cust_bill ],
4531 warn "$me _items_pkg searching for all package line items\n"
4534 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4536 warn "$me _items_pkg filtering line items\n"
4538 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4540 if ($options{section} && $options{section}->{condensed}) {
4542 warn "$me _items_pkg condensing section\n"
4546 local $Storable::canonical = 1;
4547 foreach ( @items ) {
4549 delete $item->{ref};
4550 delete $item->{ext_description};
4551 my $key = freeze($item);
4552 $itemshash{$key} ||= 0;
4553 $itemshash{$key} ++; # += $item->{quantity};
4555 @items = sort { $a->{description} cmp $b->{description} }
4556 map { my $i = thaw($_);
4557 $i->{quantity} = $itemshash{$_};
4559 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4565 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4572 return 0 unless $a->itemdesc cmp $b->itemdesc;
4573 return -1 if $b->itemdesc eq 'Tax';
4574 return 1 if $a->itemdesc eq 'Tax';
4575 return -1 if $b->itemdesc eq 'Other surcharges';
4576 return 1 if $a->itemdesc eq 'Other surcharges';
4577 $a->itemdesc cmp $b->itemdesc;
4582 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4583 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4586 sub _items_cust_bill_pkg {
4588 my $cust_bill_pkgs = shift;
4591 my $format = $opt{format} || '';
4592 my $escape_function = $opt{escape_function} || sub { shift };
4593 my $format_function = $opt{format_function} || '';
4594 my $unsquelched = $opt{unsquelched} || '';
4595 my $section = $opt{section}->{description} if $opt{section};
4596 my $summary_page = $opt{summary_page} || '';
4597 my $multilocation = $opt{multilocation} || '';
4598 my $multisection = $opt{multisection} || '';
4599 my $discount_show_always = 0;
4601 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4604 my ($s, $r, $u) = ( undef, undef, undef );
4605 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4608 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4609 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4612 foreach my $display ( grep { defined($section)
4613 ? $_->section eq $section
4616 #grep { !$_->summary || !$summary_page } # bunk!
4617 grep { !$_->summary || $multisection }
4618 $cust_bill_pkg->cust_bill_pkg_display
4622 warn "$me _items_cust_bill_pkg considering display item $display\n"
4625 my $type = $display->type;
4627 my $desc = $cust_bill_pkg->desc;
4628 $desc = substr($desc, 0, $maxlength). '...'
4629 if $format eq 'latex' && length($desc) > $maxlength;
4631 my %details_opt = ( 'format' => $format,
4632 'escape_function' => $escape_function,
4633 'format_function' => $format_function,
4636 if ( $cust_bill_pkg->pkgnum > 0 ) {
4638 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4641 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4643 if ( (!$type || $type eq 'S')
4644 && ( $cust_bill_pkg->setup != 0
4645 || $cust_bill_pkg->setup_show_zero
4650 warn "$me _items_cust_bill_pkg adding setup\n"
4653 my $description = $desc;
4654 $description .= ' Setup'
4655 if $cust_bill_pkg->recur != 0
4656 || $discount_show_always
4657 || $cust_bill_pkg->recur_show_zero;
4660 unless ( $cust_pkg->part_pkg->hide_svc_detail
4661 || $cust_bill_pkg->hidden )
4664 push @d, map &{$escape_function}($_),
4665 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4666 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4668 if ( $multilocation ) {
4669 my $loc = $cust_pkg->location_label;
4670 $loc = substr($loc, 0, $maxlength). '...'
4671 if $format eq 'latex' && length($loc) > $maxlength;
4672 push @d, &{$escape_function}($loc);
4677 push @d, $cust_bill_pkg->details(%details_opt)
4678 if $cust_bill_pkg->recur == 0;
4680 if ( $cust_bill_pkg->hidden ) {
4681 $s->{amount} += $cust_bill_pkg->setup;
4682 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4683 push @{ $s->{ext_description} }, @d;
4687 description => $description,
4688 #pkgpart => $part_pkg->pkgpart,
4689 pkgnum => $cust_bill_pkg->pkgnum,
4690 amount => $cust_bill_pkg->setup,
4691 unit_amount => $cust_bill_pkg->unitsetup,
4692 quantity => $cust_bill_pkg->quantity,
4693 ext_description => \@d,
4699 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4701 $cust_bill_pkg->recur != 0
4702 || $cust_bill_pkg->setup == 0
4703 || $discount_show_always
4704 || $cust_bill_pkg->recur_show_zero
4709 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4712 my $is_summary = $display->summary;
4713 my $description = ($is_summary && $type && $type eq 'U')
4714 ? "Usage charges" : $desc;
4716 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4717 " - ". time2str($date_format, $cust_bill_pkg->edate).
4719 unless $conf->exists('disable_line_item_date_ranges')
4720 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4724 #at least until cust_bill_pkg has "past" ranges in addition to
4725 #the "future" sdate/edate ones... see #3032
4726 my @dates = ( $self->_date );
4727 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4728 push @dates, $prev->sdate if $prev;
4729 push @dates, undef if !$prev;
4731 unless ( $cust_pkg->part_pkg->hide_svc_detail
4732 || $cust_bill_pkg->itemdesc
4733 || $cust_bill_pkg->hidden
4734 || $is_summary && $type && $type eq 'U' )
4737 warn "$me _items_cust_bill_pkg adding service details\n"
4740 push @d, map &{$escape_function}($_),
4741 $cust_pkg->h_labels_short(@dates, 'I')
4742 #$cust_bill_pkg->edate,
4743 #$cust_bill_pkg->sdate)
4744 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4746 warn "$me _items_cust_bill_pkg done adding service details\n"
4749 if ( $multilocation ) {
4750 my $loc = $cust_pkg->location_label;
4751 $loc = substr($loc, 0, $maxlength). '...'
4752 if $format eq 'latex' && length($loc) > $maxlength;
4753 push @d, &{$escape_function}($loc);
4758 unless ( $is_summary ) {
4759 warn "$me _items_cust_bill_pkg adding details\n"
4762 #instead of omitting details entirely in this case (unwanted side
4763 # effects), just omit CDRs
4764 $details_opt{'format_function'} = sub { () }
4765 if $type && $type eq 'R';
4767 push @d, $cust_bill_pkg->details(%details_opt);
4770 warn "$me _items_cust_bill_pkg calculating amount\n"
4775 $amount = $cust_bill_pkg->recur;
4776 } elsif ($type eq 'R') {
4777 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4778 } elsif ($type eq 'U') {
4779 $amount = $cust_bill_pkg->usage;
4782 if ( !$type || $type eq 'R' ) {
4784 warn "$me _items_cust_bill_pkg adding recur\n"
4787 if ( $cust_bill_pkg->hidden ) {
4788 $r->{amount} += $amount;
4789 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4790 push @{ $r->{ext_description} }, @d;
4793 description => $description,
4794 #pkgpart => $part_pkg->pkgpart,
4795 pkgnum => $cust_bill_pkg->pkgnum,
4797 unit_amount => $cust_bill_pkg->unitrecur,
4798 quantity => $cust_bill_pkg->quantity,
4799 ext_description => \@d,
4803 } else { # $type eq 'U'
4805 warn "$me _items_cust_bill_pkg adding usage\n"
4808 if ( $cust_bill_pkg->hidden ) {
4809 $u->{amount} += $amount;
4810 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4811 push @{ $u->{ext_description} }, @d;
4814 description => $description,
4815 #pkgpart => $part_pkg->pkgpart,
4816 pkgnum => $cust_bill_pkg->pkgnum,
4818 unit_amount => $cust_bill_pkg->unitrecur,
4819 quantity => $cust_bill_pkg->quantity,
4820 ext_description => \@d,
4825 } # recurring or usage with recurring charge
4827 } else { #pkgnum tax or one-shot line item (??)
4829 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4832 if ( $cust_bill_pkg->setup != 0 ) {
4834 'description' => $desc,
4835 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4838 if ( $cust_bill_pkg->recur != 0 ) {
4840 'description' => "$desc (".
4841 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4842 time2str($date_format, $cust_bill_pkg->edate). ')',
4843 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4851 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4852 && $conf->exists('discount-show-always'));
4854 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4855 if ( $_ && !$cust_bill_pkg->hidden ) {
4856 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4857 $_->{amount} =~ s/^\-0\.00$/0.00/;
4858 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4860 if $_->{amount} != 0
4861 || $discount_show_always
4862 || ( ! $_->{_is_setup} && $cust_bill_pkg->recur_show_zero )
4863 || ( $_->{_is_setup} && $cust_bill_pkg->setup_show_zero )
4871 #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4873 # $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4874 # $_->{amount} =~ s/^\-0\.00$/0.00/;
4875 # $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4877 # if $_->{amount} != 0
4878 # || $discount_show_always
4882 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4889 sub _items_credits {
4890 my( $self, %opt ) = @_;
4891 my $trim_len = $opt{'trim_len'} || 60;
4895 foreach ( $self->cust_credited ) {
4897 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4899 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4900 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4901 $reason = " ($reason) " if $reason;
4904 #'description' => 'Credit ref\#'. $_->crednum.
4905 # " (". time2str("%x",$_->cust_credit->_date) .")".
4907 'description' => 'Credit applied '.
4908 time2str($date_format,$_->cust_credit->_date). $reason,
4909 'amount' => sprintf("%.2f",$_->amount),
4917 sub _items_payments {
4921 #get & print payments
4922 foreach ( $self->cust_bill_pay ) {
4924 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4927 'description' => "Payment received ".
4928 time2str($date_format,$_->cust_pay->_date ),
4929 'amount' => sprintf("%.2f", $_->amount )
4937 =item call_details [ OPTION => VALUE ... ]
4939 Returns an array of CSV strings representing the call details for this invoice
4940 The only option available is the boolean prepend_billed_number
4945 my ($self, %opt) = @_;
4947 my $format_function = sub { shift };
4949 if ($opt{prepend_billed_number}) {
4950 $format_function = sub {
4954 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4959 my @details = map { $_->details( 'format_function' => $format_function,
4960 'escape_function' => sub{ return() },
4964 $self->cust_bill_pkg;
4965 my $header = $details[0];
4966 ( $header, grep { $_ ne $header } @details );
4976 =item process_reprint
4980 sub process_reprint {
4981 process_re_X('print', @_);
4984 =item process_reemail
4988 sub process_reemail {
4989 process_re_X('email', @_);
4997 process_re_X('fax', @_);
5005 process_re_X('ftp', @_);
5012 sub process_respool {
5013 process_re_X('spool', @_);
5016 use Storable qw(thaw);
5020 my( $method, $job ) = ( shift, shift );
5021 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5023 my $param = thaw(decode_base64(shift));
5024 warn Dumper($param) if $DEBUG;
5035 my($method, $job, %param ) = @_;
5037 warn "re_X $method for job $job with param:\n".
5038 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5041 #some false laziness w/search/cust_bill.html
5043 my $orderby = 'ORDER BY cust_bill._date';
5045 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5047 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5049 my @cust_bill = qsearch( {
5050 #'select' => "cust_bill.*",
5051 'table' => 'cust_bill',
5052 'addl_from' => $addl_from,
5054 'extra_sql' => $extra_sql,
5055 'order_by' => $orderby,
5059 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5061 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5064 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5065 foreach my $cust_bill ( @cust_bill ) {
5066 $cust_bill->$method();
5068 if ( $job ) { #progressbar foo
5070 if ( time - $min_sec > $last ) {
5071 my $error = $job->update_statustext(
5072 int( 100 * $num / scalar(@cust_bill) )
5074 die $error if $error;
5085 =head1 CLASS METHODS
5091 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5096 my ($class, $start, $end) = @_;
5098 $class->paid_sql($start, $end). ' - '.
5099 $class->credited_sql($start, $end);
5104 Returns an SQL fragment to retreive the net amount (charged minus credited).
5109 my ($class, $start, $end) = @_;
5110 'charged - '. $class->credited_sql($start, $end);
5115 Returns an SQL fragment to retreive the amount paid against this invoice.
5120 my ($class, $start, $end) = @_;
5121 $start &&= "AND cust_bill_pay._date <= $start";
5122 $end &&= "AND cust_bill_pay._date > $end";
5123 $start = '' unless defined($start);
5124 $end = '' unless defined($end);
5125 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5126 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5131 Returns an SQL fragment to retreive the amount credited against this invoice.
5136 my ($class, $start, $end) = @_;
5137 $start &&= "AND cust_credit_bill._date <= $start";
5138 $end &&= "AND cust_credit_bill._date > $end";
5139 $start = '' unless defined($start);
5140 $end = '' unless defined($end);
5141 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5142 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5147 Returns an SQL fragment to retrieve the due date of an invoice.
5148 Currently only supported on PostgreSQL.
5156 cust_bill.invoice_terms,
5157 cust_main.invoice_terms,
5158 \''.($conf->config('invoice_default_terms') || '').'\'
5159 ), E\'Net (\\\\d+)\'
5161 ) * 86400 + cust_bill._date'
5164 =item search_sql_where HASHREF
5166 Class method which returns an SQL WHERE fragment to search for parameters
5167 specified in HASHREF. Valid parameters are
5173 List reference of start date, end date, as UNIX timestamps.
5183 List reference of charged limits (exclusive).
5187 List reference of charged limits (exclusive).
5191 flag, return open invoices only
5195 flag, return net invoices only
5199 =item newest_percust
5203 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5207 sub search_sql_where {
5208 my($class, $param) = @_;
5210 warn "$me search_sql_where called with params: \n".
5211 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5217 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5218 push @search, "cust_main.agentnum = $1";
5222 if ( $param->{_date} ) {
5223 my($beginning, $ending) = @{$param->{_date}};
5225 push @search, "cust_bill._date >= $beginning",
5226 "cust_bill._date < $ending";
5230 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5231 push @search, "cust_bill.invnum >= $1";
5233 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5234 push @search, "cust_bill.invnum <= $1";
5238 if ( $param->{charged} ) {
5239 my @charged = ref($param->{charged})
5240 ? @{ $param->{charged} }
5241 : ($param->{charged});
5243 push @search, map { s/^charged/cust_bill.charged/; $_; }
5247 my $owed_sql = FS::cust_bill->owed_sql;
5250 if ( $param->{owed} ) {
5251 my @owed = ref($param->{owed})
5252 ? @{ $param->{owed} }
5254 push @search, map { s/^owed/$owed_sql/; $_; }
5259 push @search, "0 != $owed_sql"
5260 if $param->{'open'};
5261 push @search, '0 != '. FS::cust_bill->net_sql
5265 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5266 if $param->{'days'};
5269 if ( $param->{'newest_percust'} ) {
5271 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5272 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5274 my @newest_where = map { my $x = $_;
5275 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5278 grep ! /^cust_main./, @search;
5279 my $newest_where = scalar(@newest_where)
5280 ? ' AND '. join(' AND ', @newest_where)
5284 push @search, "cust_bill._date = (
5285 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5286 WHERE newest_cust_bill.custnum = cust_bill.custnum
5292 #agent virtualization
5293 my $curuser = $FS::CurrentUser::CurrentUser;
5294 if ( $curuser->username eq 'fs_queue'
5295 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5297 my $newuser = qsearchs('access_user', {
5298 'username' => $username,
5302 $curuser = $newuser;
5304 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5307 push @search, $curuser->agentnums_sql;
5309 join(' AND ', @search );
5321 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5322 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base