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
2354 my( $self, %params ) = @_;
2355 my $today = $params{today} ? $params{today} : time;
2356 warn "$me print_generic called on $self with suffix $params{template}\n"
2359 my $format = $params{format};
2360 die "Unknown format: $format"
2361 unless $format =~ /^(latex|html|template)$/;
2363 my $cust_main = $self->cust_main;
2364 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2365 unless $cust_main->payname
2366 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2368 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2369 'html' => [ '<%=', '%>' ],
2370 'template' => [ '{', '}' ],
2373 warn "$me print_generic creating template\n"
2376 #create the template
2377 my $template = $params{template} ? $params{template} : $self->_agent_template;
2378 my $templatefile = "invoice_$format";
2379 $templatefile .= "_$template"
2380 if length($template) && $conf->exists($templatefile."_$template");
2381 my @invoice_template = map "$_\n", $conf->config($templatefile)
2382 or die "cannot load config data $templatefile";
2385 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2386 #change this to a die when the old code is removed
2387 warn "old-style invoice template $templatefile; ".
2388 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2389 $old_latex = 'true';
2390 @invoice_template = _translate_old_latex_format(@invoice_template);
2393 warn "$me print_generic creating T:T object\n"
2396 my $text_template = new Text::Template(
2398 SOURCE => \@invoice_template,
2399 DELIMITERS => $delimiters{$format},
2402 warn "$me print_generic compiling T:T object\n"
2405 $text_template->compile()
2406 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2409 # additional substitution could possibly cause breakage in existing templates
2410 my %convert_maps = (
2412 'notes' => sub { map "$_", @_ },
2413 'footer' => sub { map "$_", @_ },
2414 'smallfooter' => sub { map "$_", @_ },
2415 'returnaddress' => sub { map "$_", @_ },
2416 'coupon' => sub { map "$_", @_ },
2417 'summary' => sub { map "$_", @_ },
2423 s/%%(.*)$/<!-- $1 -->/g;
2424 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2425 s/\\begin\{enumerate\}/<ol>/g;
2427 s/\\end\{enumerate\}/<\/ol>/g;
2428 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2437 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2439 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2444 s/\\\\\*?\s*$/<BR>/;
2445 s/\\hyphenation\{[\w\s\-]+}//;
2450 'coupon' => sub { "" },
2451 'summary' => sub { "" },
2458 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2459 s/\\begin\{enumerate\}//g;
2461 s/\\end\{enumerate\}//g;
2462 s/\\textbf\{(.*)\}/$1/g;
2469 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2471 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2476 s/\\\\\*?\s*$/\n/; # dubious
2477 s/\\hyphenation\{[\w\s\-]+}//;
2481 'coupon' => sub { "" },
2482 'summary' => sub { "" },
2487 # hashes for differing output formats
2488 my %nbsps = ( 'latex' => '~',
2489 'html' => '', # '&nbps;' would be nice
2490 'template' => '', # not used
2492 my $nbsp = $nbsps{$format};
2494 my %escape_functions = ( 'latex' => \&_latex_escape,
2495 'html' => \&_html_escape_nbsp,#\&encode_entities,
2496 'template' => sub { shift },
2498 my $escape_function = $escape_functions{$format};
2499 my $escape_function_nonbsp = ($format eq 'html')
2500 ? \&_html_escape : $escape_function;
2502 my %date_formats = ( 'latex' => $date_format_long,
2503 'html' => $date_format_long,
2506 $date_formats{'html'} =~ s/ / /g;
2508 my $date_format = $date_formats{$format};
2510 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2512 'html' => sub { return '<b>'. shift(). '</b>'
2514 'template' => sub { shift },
2516 my $embolden_function = $embolden_functions{$format};
2518 my %newline_tokens = ( 'latex' => '\\\\',
2522 my $newline_token = $newline_tokens{$format};
2524 warn "$me generating template variables\n"
2527 # generate template variables
2530 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2534 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2540 $returnaddress = join("\n",
2541 $conf->config_orbase("invoice_${format}returnaddress", $template)
2544 } elsif ( grep /\S/,
2545 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2547 my $convert_map = $convert_maps{$format}{'returnaddress'};
2550 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2555 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2557 my $convert_map = $convert_maps{$format}{'returnaddress'};
2558 $returnaddress = join( "\n", &$convert_map(
2559 map { s/( {2,})/'~' x length($1)/eg;
2563 ( $conf->config('company_name', $self->cust_main->agentnum),
2564 $conf->config('company_address', $self->cust_main->agentnum),
2571 my $warning = "Couldn't find a return address; ".
2572 "do you need to set the company_address configuration value?";
2574 $returnaddress = $nbsp;
2575 #$returnaddress = $warning;
2579 warn "$me generating invoice data\n"
2582 my $agentnum = $self->cust_main->agentnum;
2584 my %invoice_data = (
2587 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2588 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2589 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2590 'returnaddress' => $returnaddress,
2591 'agent' => &$escape_function($cust_main->agent->agent),
2594 'invnum' => $self->invnum,
2595 'date' => time2str($date_format, $self->_date),
2596 'today' => time2str($date_format_long, $today),
2597 'terms' => $self->terms,
2598 'template' => $template, #params{'template'},
2599 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2600 'current_charges' => sprintf("%.2f", $self->charged),
2601 'duedate' => $self->due_date2str($rdate_format), #date_format?
2604 'custnum' => $cust_main->display_custnum,
2605 'agent_custid' => &$escape_function($cust_main->agent_custid),
2606 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2607 payname company address1 address2 city state zip fax
2611 'ship_enable' => $conf->exists('invoice-ship_address'),
2612 'unitprices' => $conf->exists('invoice-unitprice'),
2613 'smallernotes' => $conf->exists('invoice-smallernotes'),
2614 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2615 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2617 #layout info -- would be fancy to calc some of this and bury the template
2619 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2620 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2621 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2622 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2623 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2624 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2625 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2626 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2627 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2628 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2630 # better hang on to conf_dir for a while (for old templates)
2631 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2633 #these are only used when doing paged plaintext
2639 my $min_sdate = 999999999999;
2641 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2642 next unless $cust_bill_pkg->pkgnum > 0;
2643 $min_sdate = $cust_bill_pkg->sdate
2644 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2645 $max_edate = $cust_bill_pkg->edate
2646 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2649 $invoice_data{'bill_period'} = '';
2650 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2651 . " to " . time2str('%e %h', $max_edate)
2652 if ($max_edate != 0 && $min_sdate != 999999999999);
2654 $invoice_data{finance_section} = '';
2655 if ( $conf->config('finance_pkgclass') ) {
2657 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2658 $invoice_data{finance_section} = $pkg_class->categoryname;
2660 $invoice_data{finance_amount} = '0.00';
2661 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2663 my $countrydefault = $conf->config('countrydefault') || 'US';
2664 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2665 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2666 my $method = $prefix.$_;
2667 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2669 $invoice_data{'ship_country'} = ''
2670 if ( $invoice_data{'ship_country'} eq $countrydefault );
2672 $invoice_data{'cid'} = $params{'cid'}
2675 if ( $cust_main->country eq $countrydefault ) {
2676 $invoice_data{'country'} = '';
2678 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2682 $invoice_data{'address'} = \@address;
2684 $cust_main->payname.
2685 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2686 ? " (P.O. #". $cust_main->payinfo. ")"
2690 push @address, $cust_main->company
2691 if $cust_main->company;
2692 push @address, $cust_main->address1;
2693 push @address, $cust_main->address2
2694 if $cust_main->address2;
2696 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2697 push @address, $invoice_data{'country'}
2698 if $invoice_data{'country'};
2700 while (scalar(@address) < 5);
2702 $invoice_data{'logo_file'} = $params{'logo_file'}
2703 if $params{'logo_file'};
2704 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2705 if $params{'barcode_file'};
2706 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2707 if $params{'barcode_img'};
2708 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2709 if $params{'barcode_cid'};
2711 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2712 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2713 #my $balance_due = $self->owed + $pr_total - $cr_total;
2714 my $balance_due = $self->owed + $pr_total;
2715 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2716 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2717 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2718 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2720 my $summarypage = '';
2721 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2724 $invoice_data{'summarypage'} = $summarypage;
2726 warn "$me substituting variables in notes, footer, smallfooter\n"
2729 my @include = (qw( notes footer smallfooter ));
2730 push @include, 'coupon' unless $params{'no_coupon'};
2731 foreach my $include (@include) {
2733 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2736 if ( $conf->exists($inc_file, $agentnum)
2737 && length( $conf->config($inc_file, $agentnum) ) ) {
2739 @inc_src = $conf->config($inc_file, $agentnum);
2743 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2745 my $convert_map = $convert_maps{$format}{$include};
2747 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2748 s/--\@\]/$delimiters{$format}[1]/g;
2751 &$convert_map( $conf->config($inc_file, $agentnum) );
2755 my $inc_tt = new Text::Template (
2757 SOURCE => [ map "$_\n", @inc_src ],
2758 DELIMITERS => $delimiters{$format},
2759 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2761 unless ( $inc_tt->compile() ) {
2762 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2763 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2767 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2769 $invoice_data{$include} =~ s/\n+$//
2770 if ($format eq 'latex');
2773 $invoice_data{'po_line'} =
2774 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2775 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2778 my %money_chars = ( 'latex' => '',
2779 'html' => $conf->config('money_char') || '$',
2782 my $money_char = $money_chars{$format};
2784 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2785 'html' => $conf->config('money_char') || '$',
2788 my $other_money_char = $other_money_chars{$format};
2789 $invoice_data{'dollar'} = $other_money_char;
2791 my @detail_items = ();
2792 my @total_items = ();
2796 $invoice_data{'detail_items'} = \@detail_items;
2797 $invoice_data{'total_items'} = \@total_items;
2798 $invoice_data{'buf'} = \@buf;
2799 $invoice_data{'sections'} = \@sections;
2801 warn "$me generating sections\n"
2804 my $previous_section = { 'description' => 'Previous Charges',
2805 'subtotal' => $other_money_char.
2806 sprintf('%.2f', $pr_total),
2807 'summarized' => $summarypage ? 'Y' : '',
2809 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2810 join(' / ', map { $cust_main->balance_date_range(@$_) }
2811 $self->_prior_month30s
2813 if $conf->exists('invoice_include_aging');
2816 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2817 'subtotal' => $taxtotal, # adjusted below
2818 'summarized' => $summarypage ? 'Y' : '',
2820 my $tax_weight = _pkg_category($tax_section->{description})
2821 ? _pkg_category($tax_section->{description})->weight
2823 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2824 $tax_section->{'sort_weight'} = $tax_weight;
2827 my $adjusttotal = 0;
2828 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2829 'subtotal' => 0, # adjusted below
2830 'summarized' => $summarypage ? 'Y' : '',
2832 my $adjust_weight = _pkg_category($adjust_section->{description})
2833 ? _pkg_category($adjust_section->{description})->weight
2835 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2836 $adjust_section->{'sort_weight'} = $adjust_weight;
2838 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2839 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2840 $invoice_data{'multisection'} = $multisection;
2841 my $late_sections = [];
2842 my $extra_sections = [];
2843 my $extra_lines = ();
2844 if ( $multisection ) {
2845 ($extra_sections, $extra_lines) =
2846 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2847 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2849 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2851 push @detail_items, @$extra_lines if $extra_lines;
2853 $self->_items_sections( $late_sections, # this could stand a refactor
2855 $escape_function_nonbsp,
2859 if ($conf->exists('svc_phone_sections')) {
2860 my ($phone_sections, $phone_lines) =
2861 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2862 push @{$late_sections}, @$phone_sections;
2863 push @detail_items, @$phone_lines;
2865 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2866 my ($accountcode_section, $accountcode_lines) =
2867 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2868 if ( scalar(@$accountcode_lines) ) {
2869 push @{$late_sections}, $accountcode_section;
2870 push @detail_items, @$accountcode_lines;
2874 push @sections, { 'description' => '', 'subtotal' => '' };
2877 unless ( $conf->exists('disable_previous_balance')
2878 || $conf->exists('previous_balance-summary_only')
2882 warn "$me adding previous balances\n"
2885 foreach my $line_item ( $self->_items_previous ) {
2888 ext_description => [],
2890 $detail->{'ref'} = $line_item->{'pkgnum'};
2891 $detail->{'quantity'} = 1;
2892 $detail->{'section'} = $previous_section;
2893 $detail->{'description'} = &$escape_function($line_item->{'description'});
2894 if ( exists $line_item->{'ext_description'} ) {
2895 @{$detail->{'ext_description'}} = map {
2896 &$escape_function($_);
2897 } @{$line_item->{'ext_description'}};
2899 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2900 $line_item->{'amount'};
2901 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2903 push @detail_items, $detail;
2904 push @buf, [ $detail->{'description'},
2905 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2911 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2912 push @buf, ['','-----------'];
2913 push @buf, [ 'Total Previous Balance',
2914 $money_char. sprintf("%10.2f", $pr_total) ];
2918 if ( $conf->exists('svc_phone-did-summary') ) {
2919 warn "$me adding DID summary\n"
2922 my ($didsummary,$minutes) = $self->_did_summary;
2923 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2925 { 'description' => $didsummary_desc,
2926 'ext_description' => [ $didsummary, $minutes ],
2930 foreach my $section (@sections, @$late_sections) {
2932 warn "$me adding section \n". Dumper($section)
2935 # begin some normalization
2936 $section->{'subtotal'} = $section->{'amount'}
2938 && !exists($section->{subtotal})
2939 && exists($section->{amount});
2941 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2942 if ( $invoice_data{finance_section} &&
2943 $section->{'description'} eq $invoice_data{finance_section} );
2945 $section->{'subtotal'} = $other_money_char.
2946 sprintf('%.2f', $section->{'subtotal'})
2949 # continue some normalization
2950 $section->{'amount'} = $section->{'subtotal'}
2954 if ( $section->{'description'} ) {
2955 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2960 warn "$me setting options\n"
2963 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2965 $options{'section'} = $section if $multisection;
2966 $options{'format'} = $format;
2967 $options{'escape_function'} = $escape_function;
2968 $options{'format_function'} = sub { () } unless $unsquelched;
2969 $options{'unsquelched'} = $unsquelched;
2970 $options{'summary_page'} = $summarypage;
2971 $options{'skip_usage'} =
2972 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2973 $options{'multilocation'} = $multilocation;
2974 $options{'multisection'} = $multisection;
2976 warn "$me searching for line items\n"
2979 foreach my $line_item ( $self->_items_pkg(%options) ) {
2981 warn "$me adding line item $line_item\n"
2985 ext_description => [],
2987 $detail->{'ref'} = $line_item->{'pkgnum'};
2988 $detail->{'quantity'} = $line_item->{'quantity'};
2989 $detail->{'section'} = $section;
2990 $detail->{'description'} = &$escape_function($line_item->{'description'});
2991 if ( exists $line_item->{'ext_description'} ) {
2992 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2994 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2995 $line_item->{'amount'};
2996 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2997 $line_item->{'unit_amount'};
2998 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3000 push @detail_items, $detail;
3001 push @buf, ( [ $detail->{'description'},
3002 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3004 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3008 if ( $section->{'description'} ) {
3009 push @buf, ( ['','-----------'],
3010 [ $section->{'description'}. ' sub-total',
3011 $money_char. sprintf("%10.2f", $section->{'subtotal'})
3020 $invoice_data{current_less_finance} =
3021 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3023 if ( $multisection && !$conf->exists('disable_previous_balance')
3024 || $conf->exists('previous_balance-summary_only') )
3026 unshift @sections, $previous_section if $pr_total;
3029 warn "$me adding taxes\n"
3032 foreach my $tax ( $self->_items_tax ) {
3034 $taxtotal += $tax->{'amount'};
3036 my $description = &$escape_function( $tax->{'description'} );
3037 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3039 if ( $multisection ) {
3041 my $money = $old_latex ? '' : $money_char;
3042 push @detail_items, {
3043 ext_description => [],
3046 description => $description,
3047 amount => $money. $amount,
3049 section => $tax_section,
3054 push @total_items, {
3055 'total_item' => $description,
3056 'total_amount' => $other_money_char. $amount,
3061 push @buf,[ $description,
3062 $money_char. $amount,
3069 $total->{'total_item'} = 'Sub-total';
3070 $total->{'total_amount'} =
3071 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3073 if ( $multisection ) {
3074 $tax_section->{'subtotal'} = $other_money_char.
3075 sprintf('%.2f', $taxtotal);
3076 $tax_section->{'pretotal'} = 'New charges sub-total '.
3077 $total->{'total_amount'};
3078 push @sections, $tax_section if $taxtotal;
3080 unshift @total_items, $total;
3083 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3085 push @buf,['','-----------'];
3086 push @buf,[( $conf->exists('disable_previous_balance')
3088 : 'Total New Charges'
3090 $money_char. sprintf("%10.2f",$self->charged) ];
3096 $item = $conf->config('previous_balance-exclude_from_total')
3097 || 'Total New Charges'
3098 if $conf->exists('previous_balance-exclude_from_total');
3099 my $amount = $self->charged +
3100 ( $conf->exists('disable_previous_balance') ||
3101 $conf->exists('previous_balance-exclude_from_total')
3105 $total->{'total_item'} = &$embolden_function($item);
3106 $total->{'total_amount'} =
3107 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3108 if ( $multisection ) {
3109 if ( $adjust_section->{'sort_weight'} ) {
3110 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3111 sprintf("%.2f", ($self->billing_balance || 0) );
3113 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3114 sprintf('%.2f', $self->charged );
3117 push @total_items, $total;
3119 push @buf,['','-----------'];
3122 sprintf( '%10.2f', $amount )
3127 unless ( $conf->exists('disable_previous_balance') ) {
3128 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3131 my $credittotal = 0;
3132 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3135 $total->{'total_item'} = &$escape_function($credit->{'description'});
3136 $credittotal += $credit->{'amount'};
3137 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3138 $adjusttotal += $credit->{'amount'};
3139 if ( $multisection ) {
3140 my $money = $old_latex ? '' : $money_char;
3141 push @detail_items, {
3142 ext_description => [],
3145 description => &$escape_function($credit->{'description'}),
3146 amount => $money. $credit->{'amount'},
3148 section => $adjust_section,
3151 push @total_items, $total;
3155 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3158 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3159 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3163 my $paymenttotal = 0;
3164 foreach my $payment ( $self->_items_payments ) {
3166 $total->{'total_item'} = &$escape_function($payment->{'description'});
3167 $paymenttotal += $payment->{'amount'};
3168 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3169 $adjusttotal += $payment->{'amount'};
3170 if ( $multisection ) {
3171 my $money = $old_latex ? '' : $money_char;
3172 push @detail_items, {
3173 ext_description => [],
3176 description => &$escape_function($payment->{'description'}),
3177 amount => $money. $payment->{'amount'},
3179 section => $adjust_section,
3182 push @total_items, $total;
3184 push @buf, [ $payment->{'description'},
3185 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3188 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3190 if ( $multisection ) {
3191 $adjust_section->{'subtotal'} = $other_money_char.
3192 sprintf('%.2f', $adjusttotal);
3193 push @sections, $adjust_section
3194 unless $adjust_section->{sort_weight};
3199 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3200 $total->{'total_amount'} =
3201 &$embolden_function(
3202 $other_money_char. sprintf('%.2f', $summarypage
3204 $self->billing_balance
3205 : $self->owed + $pr_total
3208 if ( $multisection && !$adjust_section->{sort_weight} ) {
3209 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3210 $total->{'total_amount'};
3212 push @total_items, $total;
3214 push @buf,['','-----------'];
3215 push @buf,[$self->balance_due_msg, $money_char.
3216 sprintf("%10.2f", $balance_due ) ];
3219 if ( $conf->exists('previous_balance-show_credit')
3220 and $cust_main->balance < 0 ) {
3221 my $credit_total = {
3222 'total_item' => &$embolden_function($self->credit_balance_msg),
3223 'total_amount' => &$embolden_function(
3224 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3227 if ( $multisection ) {
3228 $adjust_section->{'posttotal'} .= $newline_token .
3229 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3232 push @total_items, $credit_total;
3234 push @buf,['','-----------'];
3235 push @buf,[$self->credit_balance_msg, $money_char.
3236 sprintf("%10.2f", -$cust_main->balance ) ];
3240 if ( $multisection ) {
3241 if ($conf->exists('svc_phone_sections')) {
3243 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3244 $total->{'total_amount'} =
3245 &$embolden_function(
3246 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3248 my $last_section = pop @sections;
3249 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3250 $total->{'total_amount'};
3251 push @sections, $last_section;
3253 push @sections, @$late_sections
3257 my @includelist = ();
3258 push @includelist, 'summary' if $summarypage;
3259 foreach my $include ( @includelist ) {
3261 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3264 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3266 @inc_src = $conf->config($inc_file, $agentnum);
3270 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3272 my $convert_map = $convert_maps{$format}{$include};
3274 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3275 s/--\@\]/$delimiters{$format}[1]/g;
3278 &$convert_map( $conf->config($inc_file, $agentnum) );
3282 my $inc_tt = new Text::Template (
3284 SOURCE => [ map "$_\n", @inc_src ],
3285 DELIMITERS => $delimiters{$format},
3286 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3288 unless ( $inc_tt->compile() ) {
3289 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3290 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3294 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3296 $invoice_data{$include} =~ s/\n+$//
3297 if ($format eq 'latex');
3302 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3303 /invoice_lines\((\d*)\)/;
3304 $invoice_lines += $1 || scalar(@buf);
3307 die "no invoice_lines() functions in template?"
3308 if ( $format eq 'template' && !$wasfunc );
3310 if ($format eq 'template') {
3312 if ( $invoice_lines ) {
3313 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3314 $invoice_data{'total_pages'}++
3315 if scalar(@buf) % $invoice_lines;
3318 #setup subroutine for the template
3319 sub FS::cust_bill::_template::invoice_lines {
3320 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3322 scalar(@FS::cust_bill::_template::buf)
3323 ? shift @FS::cust_bill::_template::buf
3332 push @collect, split("\n",
3333 $text_template->fill_in( HASH => \%invoice_data,
3334 PACKAGE => 'FS::cust_bill::_template'
3337 $FS::cust_bill::_template::page++;
3339 map "$_\n", @collect;
3341 warn "filling in template for invoice ". $self->invnum. "\n"
3343 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3346 $text_template->fill_in(HASH => \%invoice_data);
3350 # helper routine for generating date ranges
3351 sub _prior_month30s {
3354 [ 1, 2592000 ], # 0-30 days ago
3355 [ 2592000, 5184000 ], # 30-60 days ago
3356 [ 5184000, 7776000 ], # 60-90 days ago
3357 [ 7776000, 0 ], # 90+ days ago
3360 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3361 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3366 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3368 Returns an postscript invoice, as a scalar.
3370 Options can be passed as a hashref (recommended) or as a list of time, template
3371 and then any key/value pairs for any other options.
3373 I<time> an optional value used to control the printing of overdue messages. The
3374 default is now. It isn't the date of the invoice; that's the `_date' field.
3375 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3376 L<Time::Local> and L<Date::Parse> for conversion functions.
3378 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3385 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3386 my $ps = generate_ps($file);
3388 unlink($barcodefile) if $barcodefile;
3393 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3395 Returns an PDF invoice, as a scalar.
3397 Options can be passed as a hashref (recommended) or as a list of time, template
3398 and then any key/value pairs for any other options.
3400 I<time> an optional value used to control the printing of overdue messages. The
3401 default is now. It isn't the date of the invoice; that's the `_date' field.
3402 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3403 L<Time::Local> and L<Date::Parse> for conversion functions.
3405 I<template>, if specified, is the name of a suffix for alternate invoices.
3407 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3414 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3415 my $pdf = generate_pdf($file);
3417 unlink($barcodefile) if $barcodefile;
3422 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3424 Returns an HTML invoice, as a scalar.
3426 I<time> an optional value used to control the printing of overdue messages. The
3427 default is now. It isn't the date of the invoice; that's the `_date' field.
3428 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3429 L<Time::Local> and L<Date::Parse> for conversion functions.
3431 I<template>, if specified, is the name of a suffix for alternate invoices.
3433 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3435 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3436 when emailing the invoice as part of a multipart/related MIME email.
3444 %params = %{ shift() };
3446 $params{'time'} = shift;
3447 $params{'template'} = shift;
3448 $params{'cid'} = shift;
3451 $params{'format'} = 'html';
3453 $self->print_generic( %params );
3456 # quick subroutine for print_latex
3458 # There are ten characters that LaTeX treats as special characters, which
3459 # means that they do not simply typeset themselves:
3460 # # $ % & ~ _ ^ \ { }
3462 # TeX ignores blanks following an escaped character; if you want a blank (as
3463 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3467 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3468 $value =~ s/([<>])/\$$1\$/g;
3474 encode_entities($value);
3478 sub _html_escape_nbsp {
3479 my $value = _html_escape(shift);
3480 $value =~ s/ +/ /g;
3484 #utility methods for print_*
3486 sub _translate_old_latex_format {
3487 warn "_translate_old_latex_format called\n"
3494 if ( $line =~ /^%%Detail\s*$/ ) {
3496 push @template, q![@--!,
3497 q! foreach my $_tr_line (@detail_items) {!,
3498 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3499 q! $_tr_line->{'description'} .= !,
3500 q! "\\tabularnewline\n~~".!,
3501 q! join( "\\tabularnewline\n~~",!,
3502 q! @{$_tr_line->{'ext_description'}}!,
3506 while ( ( my $line_item_line = shift )
3507 !~ /^%%EndDetail\s*$/ ) {
3508 $line_item_line =~ s/'/\\'/g; # nice LTS
3509 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3510 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3511 push @template, " \$OUT .= '$line_item_line';";
3514 push @template, '}',
3517 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3519 push @template, '[@--',
3520 ' foreach my $_tr_line (@total_items) {';
3522 while ( ( my $total_item_line = shift )
3523 !~ /^%%EndTotalDetails\s*$/ ) {
3524 $total_item_line =~ s/'/\\'/g; # nice LTS
3525 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3526 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3527 push @template, " \$OUT .= '$total_item_line';";
3530 push @template, '}',
3534 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3535 push @template, $line;
3541 warn "$_\n" foreach @template;
3550 #check for an invoice-specific override
3551 return $self->invoice_terms if $self->invoice_terms;
3553 #check for a customer- specific override
3554 my $cust_main = $self->cust_main;
3555 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3557 #use configured default
3558 $conf->config('invoice_default_terms') || '';
3564 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3565 $duedate = $self->_date() + ( $1 * 86400 );
3572 $self->due_date ? time2str(shift, $self->due_date) : '';
3575 sub balance_due_msg {
3577 my $msg = 'Balance Due';
3578 return $msg unless $self->terms;
3579 if ( $self->due_date ) {
3580 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3581 } elsif ( $self->terms ) {
3582 $msg .= ' - '. $self->terms;
3587 sub balance_due_date {
3590 if ( $conf->exists('invoice_default_terms')
3591 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3592 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3597 sub credit_balance_msg { 'Credit Balance Remaining' }
3599 =item invnum_date_pretty
3601 Returns a string with the invoice number and date, for example:
3602 "Invoice #54 (3/20/2008)"
3606 sub invnum_date_pretty {
3608 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3613 Returns a string with the date, for example: "3/20/2008"
3619 time2str($date_format, $self->_date);
3622 use vars qw(%pkg_category_cache);
3623 sub _items_sections {
3626 my $summarypage = shift;
3628 my $extra_sections = shift;
3632 my %late_subtotal = ();
3635 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3638 my $usage = $cust_bill_pkg->usage;
3640 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3641 next if ( $display->summary && $summarypage );
3643 my $section = $display->section;
3644 my $type = $display->type;
3646 $not_tax{$section} = 1
3647 unless $cust_bill_pkg->pkgnum == 0;
3649 if ( $display->post_total && !$summarypage ) {
3650 if (! $type || $type eq 'S') {
3651 $late_subtotal{$section} += $cust_bill_pkg->setup
3652 if $cust_bill_pkg->setup != 0;
3656 $late_subtotal{$section} += $cust_bill_pkg->recur
3657 if $cust_bill_pkg->recur != 0;
3660 if ($type && $type eq 'R') {
3661 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3662 if $cust_bill_pkg->recur != 0;
3665 if ($type && $type eq 'U') {
3666 $late_subtotal{$section} += $usage
3667 unless scalar(@$extra_sections);
3672 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3674 if (! $type || $type eq 'S') {
3675 $subtotal{$section} += $cust_bill_pkg->setup
3676 if $cust_bill_pkg->setup != 0;
3680 $subtotal{$section} += $cust_bill_pkg->recur
3681 if $cust_bill_pkg->recur != 0;
3684 if ($type && $type eq 'R') {
3685 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3686 if $cust_bill_pkg->recur != 0;
3689 if ($type && $type eq 'U') {
3690 $subtotal{$section} += $usage
3691 unless scalar(@$extra_sections);
3700 %pkg_category_cache = ();
3702 push @$late, map { { 'description' => &{$escape}($_),
3703 'subtotal' => $late_subtotal{$_},
3705 'sort_weight' => ( _pkg_category($_)
3706 ? _pkg_category($_)->weight
3709 ((_pkg_category($_) && _pkg_category($_)->condense)
3710 ? $self->_condense_section($format)
3714 sort _sectionsort keys %late_subtotal;
3717 if ( $summarypage ) {
3718 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3719 map { $_->categoryname } qsearch('pkg_category', {});
3720 push @sections, '' if exists($subtotal{''});
3722 @sections = keys %subtotal;
3725 my @early = map { { 'description' => &{$escape}($_),
3726 'subtotal' => $subtotal{$_},
3727 'summarized' => $not_tax{$_} ? '' : 'Y',
3728 'tax_section' => $not_tax{$_} ? '' : 'Y',
3729 'sort_weight' => ( _pkg_category($_)
3730 ? _pkg_category($_)->weight
3733 ((_pkg_category($_) && _pkg_category($_)->condense)
3734 ? $self->_condense_section($format)
3739 push @early, @$extra_sections if $extra_sections;
3741 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3745 #helper subs for above
3748 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3752 my $categoryname = shift;
3753 $pkg_category_cache{$categoryname} ||=
3754 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3757 my %condensed_format = (
3758 'label' => [ qw( Description Qty Amount ) ],
3760 sub { shift->{description} },
3761 sub { shift->{quantity} },
3762 sub { my($href, %opt) = @_;
3763 ($opt{dollar} || ''). $href->{amount};
3766 'align' => [ qw( l r r ) ],
3767 'span' => [ qw( 5 1 1 ) ], # unitprices?
3768 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3771 sub _condense_section {
3772 my ( $self, $format ) = ( shift, shift );
3774 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3775 qw( description_generator
3778 total_line_generator
3783 sub _condensed_generator_defaults {
3784 my ( $self, $format ) = ( shift, shift );
3785 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3794 sub _condensed_header_generator {
3795 my ( $self, $format ) = ( shift, shift );
3797 my ( $f, $prefix, $suffix, $separator, $column ) =
3798 _condensed_generator_defaults($format);
3800 if ($format eq 'latex') {
3801 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3802 $suffix = "\\\\\n\\hline";
3805 sub { my ($d,$a,$s,$w) = @_;
3806 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3808 } elsif ( $format eq 'html' ) {
3809 $prefix = '<th></th>';
3813 sub { my ($d,$a,$s,$w) = @_;
3814 return qq!<th align="$html_align{$a}">$d</th>!;
3822 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3824 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3827 $prefix. join($separator, @result). $suffix;
3832 sub _condensed_description_generator {
3833 my ( $self, $format ) = ( shift, shift );
3835 my ( $f, $prefix, $suffix, $separator, $column ) =
3836 _condensed_generator_defaults($format);
3838 my $money_char = '$';
3839 if ($format eq 'latex') {
3840 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3842 $separator = " & \n";
3844 sub { my ($d,$a,$s,$w) = @_;
3845 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3847 $money_char = '\\dollar';
3848 }elsif ( $format eq 'html' ) {
3849 $prefix = '"><td align="center"></td>';
3853 sub { my ($d,$a,$s,$w) = @_;
3854 return qq!<td align="$html_align{$a}">$d</td>!;
3856 #$money_char = $conf->config('money_char') || '$';
3857 $money_char = ''; # this is madness
3865 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3867 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3869 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3870 map { $f->{$_}->[$i] } qw(align span width)
3874 $prefix. join( $separator, @result ). $suffix;
3879 sub _condensed_total_generator {
3880 my ( $self, $format ) = ( shift, shift );
3882 my ( $f, $prefix, $suffix, $separator, $column ) =
3883 _condensed_generator_defaults($format);
3886 if ($format eq 'latex') {
3889 $separator = " & \n";
3891 sub { my ($d,$a,$s,$w) = @_;
3892 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3894 }elsif ( $format eq 'html' ) {
3898 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3900 sub { my ($d,$a,$s,$w) = @_;
3901 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3910 # my $r = &{$f->{fields}->[$i]}(@args);
3911 # $r .= ' Total' unless $i;
3913 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3915 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3916 map { $f->{$_}->[$i] } qw(align span width)
3920 $prefix. join( $separator, @result ). $suffix;
3925 =item total_line_generator FORMAT
3927 Returns a coderef used for generation of invoice total line items for this
3928 usage_class. FORMAT is either html or latex
3932 # should not be used: will have issues with hash element names (description vs
3933 # total_item and amount vs total_amount -- another array of functions?
3935 sub _condensed_total_line_generator {
3936 my ( $self, $format ) = ( shift, shift );
3938 my ( $f, $prefix, $suffix, $separator, $column ) =
3939 _condensed_generator_defaults($format);
3942 if ($format eq 'latex') {
3945 $separator = " & \n";
3947 sub { my ($d,$a,$s,$w) = @_;
3948 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3950 }elsif ( $format eq 'html' ) {
3954 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3956 sub { my ($d,$a,$s,$w) = @_;
3957 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3966 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3968 &{$column}( &{$f->{fields}->[$i]}(@args),
3969 map { $f->{$_}->[$i] } qw(align span width)
3973 $prefix. join( $separator, @result ). $suffix;
3978 #sub _items_extra_usage_sections {
3980 # my $escape = shift;
3982 # my %sections = ();
3984 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3985 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3987 # next unless $cust_bill_pkg->pkgnum > 0;
3989 # foreach my $section ( keys %usage_class ) {
3991 # my $usage = $cust_bill_pkg->usage($section);
3993 # next unless $usage && $usage > 0;
3995 # $sections{$section} ||= 0;
3996 # $sections{$section} += $usage;
4002 # map { { 'description' => &{$escape}($_),
4003 # 'subtotal' => $sections{$_},
4004 # 'summarized' => '',
4005 # 'tax_section' => '',
4008 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4012 sub _items_extra_usage_sections {
4021 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4023 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4024 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4025 next unless $cust_bill_pkg->pkgnum > 0;
4027 foreach my $classnum ( keys %usage_class ) {
4028 my $section = $usage_class{$classnum}->classname;
4029 $classnums{$section} = $classnum;
4031 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4032 my $amount = $detail->amount;
4033 next unless $amount && $amount > 0;
4035 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4036 $sections{$section}{amount} += $amount; #subtotal
4037 $sections{$section}{calls}++;
4038 $sections{$section}{duration} += $detail->duration;
4040 my $desc = $detail->regionname;
4041 my $description = $desc;
4042 $description = substr($desc, 0, $maxlength). '...'
4043 if $format eq 'latex' && length($desc) > $maxlength;
4045 $lines{$section}{$desc} ||= {
4046 description => &{$escape}($description),
4047 #pkgpart => $part_pkg->pkgpart,
4048 pkgnum => $cust_bill_pkg->pkgnum,
4053 #unit_amount => $cust_bill_pkg->unitrecur,
4054 quantity => $cust_bill_pkg->quantity,
4055 product_code => 'N/A',
4056 ext_description => [],
4059 $lines{$section}{$desc}{amount} += $amount;
4060 $lines{$section}{$desc}{calls}++;
4061 $lines{$section}{$desc}{duration} += $detail->duration;
4067 my %sectionmap = ();
4068 foreach (keys %sections) {
4069 my $usage_class = $usage_class{$classnums{$_}};
4070 $sectionmap{$_} = { 'description' => &{$escape}($_),
4071 'amount' => $sections{$_}{amount}, #subtotal
4072 'calls' => $sections{$_}{calls},
4073 'duration' => $sections{$_}{duration},
4075 'tax_section' => '',
4076 'sort_weight' => $usage_class->weight,
4077 ( $usage_class->format
4078 ? ( map { $_ => $usage_class->$_($format) }
4079 qw( description_generator header_generator total_generator total_line_generator )
4086 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4090 foreach my $section ( keys %lines ) {
4091 foreach my $line ( keys %{$lines{$section}} ) {
4092 my $l = $lines{$section}{$line};
4093 $l->{section} = $sectionmap{$section};
4094 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4095 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4100 return(\@sections, \@lines);
4106 my $end = $self->_date;
4108 # start at date of previous invoice + 1 second or 0 if no previous invoice
4109 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4110 $start = 0 if !$start;
4113 my $cust_main = $self->cust_main;
4114 my @pkgs = $cust_main->all_pkgs;
4115 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4118 foreach my $pkg ( @pkgs ) {
4119 my @h_cust_svc = $pkg->h_cust_svc($end);
4120 foreach my $h_cust_svc ( @h_cust_svc ) {
4121 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4122 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4124 my $inserted = $h_cust_svc->date_inserted;
4125 my $deleted = $h_cust_svc->date_deleted;
4126 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4128 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4130 # DID either activated or ported in; cannot be both for same DID simultaneously
4131 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4132 && (!$phone_inserted->lnp_status
4133 || $phone_inserted->lnp_status eq ''
4134 || $phone_inserted->lnp_status eq 'native')) {
4137 else { # this one not so clean, should probably move to (h_)svc_phone
4138 my $phone_portedin = qsearchs( 'h_svc_phone',
4139 { 'svcnum' => $h_cust_svc->svcnum,
4140 'lnp_status' => 'portedin' },
4141 FS::h_svc_phone->sql_h_searchs($end),
4143 $num_portedin++ if $phone_portedin;
4146 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4147 if($deleted >= $start && $deleted <= $end && $phone_deleted
4148 && (!$phone_deleted->lnp_status
4149 || $phone_deleted->lnp_status ne 'portingout')) {
4152 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4153 && $phone_deleted->lnp_status
4154 && $phone_deleted->lnp_status eq 'portingout') {
4158 # increment usage minutes
4159 if ( $phone_inserted ) {
4160 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4161 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4164 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4167 # don't look at this service again
4168 push @seen, $h_cust_svc->svcnum;
4172 $minutes = sprintf("%d", $minutes);
4173 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4174 . "$num_deactivated Ported-Out: $num_portedout ",
4175 "Total Minutes: $minutes");
4178 sub _items_accountcode_cdr {
4183 my $section = { 'amount' => 0,
4186 'sort_weight' => '',
4188 'description' => 'Usage by Account Code',
4194 my %accountcodes = ();
4196 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4197 next unless $cust_bill_pkg->pkgnum > 0;
4199 my @header = $cust_bill_pkg->details_header;
4200 next unless scalar(@header);
4201 $section->{'header'} = join(',',@header);
4203 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4205 $section->{'header'} = $detail->formatted('format' => $format)
4206 if($detail->detail eq $section->{'header'});
4208 my $accountcode = $detail->accountcode;
4209 next unless $accountcode;
4211 my $amount = $detail->amount;
4212 next unless $amount && $amount > 0;
4214 $accountcodes{$accountcode} ||= {
4215 description => $accountcode,
4222 product_code => 'N/A',
4223 section => $section,
4224 ext_description => [ $section->{'header'} ],
4228 $section->{'amount'} += $amount;
4229 $accountcodes{$accountcode}{'amount'} += $amount;
4230 $accountcodes{$accountcode}{calls}++;
4231 $accountcodes{$accountcode}{duration} += $detail->duration;
4232 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4236 foreach my $l ( values %accountcodes ) {
4237 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4238 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4239 foreach my $sorted_detail ( @sorted_detail ) {
4240 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4242 delete $l->{detail_temp};
4246 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4248 return ($section,\@sorted_lines);
4251 sub _items_svc_phone_sections {
4260 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4262 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4263 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4265 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4266 next unless $cust_bill_pkg->pkgnum > 0;
4268 my @header = $cust_bill_pkg->details_header;
4269 next unless scalar(@header);
4271 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4273 my $phonenum = $detail->phonenum;
4274 next unless $phonenum;
4276 my $amount = $detail->amount;
4277 next unless $amount && $amount > 0;
4279 $sections{$phonenum} ||= { 'amount' => 0,
4282 'sort_weight' => -1,
4283 'phonenum' => $phonenum,
4285 $sections{$phonenum}{amount} += $amount; #subtotal
4286 $sections{$phonenum}{calls}++;
4287 $sections{$phonenum}{duration} += $detail->duration;
4289 my $desc = $detail->regionname;
4290 my $description = $desc;
4291 $description = substr($desc, 0, $maxlength). '...'
4292 if $format eq 'latex' && length($desc) > $maxlength;
4294 $lines{$phonenum}{$desc} ||= {
4295 description => &{$escape}($description),
4296 #pkgpart => $part_pkg->pkgpart,
4304 product_code => 'N/A',
4305 ext_description => [],
4308 $lines{$phonenum}{$desc}{amount} += $amount;
4309 $lines{$phonenum}{$desc}{calls}++;
4310 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4312 my $line = $usage_class{$detail->classnum}->classname;
4313 $sections{"$phonenum $line"} ||=
4317 'sort_weight' => $usage_class{$detail->classnum}->weight,
4318 'phonenum' => $phonenum,
4319 'header' => [ @header ],
4321 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4322 $sections{"$phonenum $line"}{calls}++;
4323 $sections{"$phonenum $line"}{duration} += $detail->duration;
4325 $lines{"$phonenum $line"}{$desc} ||= {
4326 description => &{$escape}($description),
4327 #pkgpart => $part_pkg->pkgpart,
4335 product_code => 'N/A',
4336 ext_description => [],
4339 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4340 $lines{"$phonenum $line"}{$desc}{calls}++;
4341 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4342 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4343 $detail->formatted('format' => $format);
4348 my %sectionmap = ();
4349 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4350 foreach ( keys %sections ) {
4351 my @header = @{ $sections{$_}{header} || [] };
4353 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4354 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4355 my $usage_class = $summary ? $simple : $usage_simple;
4356 my $ending = $summary ? ' usage charges' : '';
4359 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4361 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4362 'amount' => $sections{$_}{amount}, #subtotal
4363 'calls' => $sections{$_}{calls},
4364 'duration' => $sections{$_}{duration},
4366 'tax_section' => '',
4367 'phonenum' => $sections{$_}{phonenum},
4368 'sort_weight' => $sections{$_}{sort_weight},
4369 'post_total' => $summary, #inspire pagebreak
4371 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4372 qw( description_generator
4375 total_line_generator
4382 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4383 $a->{sort_weight} <=> $b->{sort_weight}
4388 foreach my $section ( keys %lines ) {
4389 foreach my $line ( keys %{$lines{$section}} ) {
4390 my $l = $lines{$section}{$line};
4391 $l->{section} = $sectionmap{$section};
4392 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4393 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4398 if($conf->exists('phone_usage_class_summary')) {
4399 # this only works with Latex
4403 # after this, we'll have only two sections per DID:
4404 # Calls Summary and Calls Detail
4405 foreach my $section ( @sections ) {
4406 if($section->{'post_total'}) {
4407 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4408 $section->{'total_line_generator'} = sub { '' };
4409 $section->{'total_generator'} = sub { '' };
4410 $section->{'header_generator'} = sub { '' };
4411 $section->{'description_generator'} = '';
4412 push @newsections, $section;
4413 my %calls_detail = %$section;
4414 $calls_detail{'post_total'} = '';
4415 $calls_detail{'sort_weight'} = '';
4416 $calls_detail{'description_generator'} = sub { '' };
4417 $calls_detail{'header_generator'} = sub {
4418 return ' & Date/Time & Called Number & Duration & Price'
4419 if $format eq 'latex';
4422 $calls_detail{'description'} = 'Calls Detail: '
4423 . $section->{'phonenum'};
4424 push @newsections, \%calls_detail;
4428 # after this, each usage class is collapsed/summarized into a single
4429 # line under the Calls Summary section
4430 foreach my $newsection ( @newsections ) {
4431 if($newsection->{'post_total'}) { # this means Calls Summary
4432 foreach my $section ( @sections ) {
4433 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4434 && !$section->{'post_total'});
4435 my $newdesc = $section->{'description'};
4436 my $tn = $section->{'phonenum'};
4437 $newdesc =~ s/$tn//g;
4438 my $line = { ext_description => [],
4442 calls => $section->{'calls'},
4443 section => $newsection,
4444 duration => $section->{'duration'},
4445 description => $newdesc,
4446 amount => sprintf("%.2f",$section->{'amount'}),
4447 product_code => 'N/A',
4449 push @newlines, $line;
4454 # after this, Calls Details is populated with all CDRs
4455 foreach my $newsection ( @newsections ) {
4456 if(!$newsection->{'post_total'}) { # this means Calls Details
4457 foreach my $line ( @lines ) {
4458 next unless (scalar(@{$line->{'ext_description'}}) &&
4459 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4461 my @extdesc = @{$line->{'ext_description'}};
4463 foreach my $extdesc ( @extdesc ) {
4464 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4465 push @newextdesc, $extdesc;
4467 $line->{'ext_description'} = \@newextdesc;
4468 $line->{'section'} = $newsection;
4469 push @newlines, $line;
4474 return(\@newsections, \@newlines);
4477 return(\@sections, \@lines);
4484 #my @display = scalar(@_)
4486 # : qw( _items_previous _items_pkg );
4487 # #: qw( _items_pkg );
4488 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4489 my @display = qw( _items_previous _items_pkg );
4492 foreach my $display ( @display ) {
4493 push @b, $self->$display(@_);
4498 sub _items_previous {
4500 my $cust_main = $self->cust_main;
4501 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4503 foreach ( @pr_cust_bill ) {
4504 my $date = $conf->exists('invoice_show_prior_due_date')
4505 ? 'due '. $_->due_date2str($date_format)
4506 : time2str($date_format, $_->_date);
4508 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4509 #'pkgpart' => 'N/A',
4511 'amount' => sprintf("%.2f", $_->owed),
4517 # 'description' => 'Previous Balance',
4518 # #'pkgpart' => 'N/A',
4519 # 'pkgnum' => 'N/A',
4520 # 'amount' => sprintf("%10.2f", $pr_total ),
4521 # 'ext_description' => [ map {
4522 # "Invoice ". $_->invnum.
4523 # " (". time2str("%x",$_->_date). ") ".
4524 # sprintf("%10.2f", $_->owed)
4525 # } @pr_cust_bill ],
4534 warn "$me _items_pkg searching for all package line items\n"
4537 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4539 warn "$me _items_pkg filtering line items\n"
4541 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4543 if ($options{section} && $options{section}->{condensed}) {
4545 warn "$me _items_pkg condensing section\n"
4549 local $Storable::canonical = 1;
4550 foreach ( @items ) {
4552 delete $item->{ref};
4553 delete $item->{ext_description};
4554 my $key = freeze($item);
4555 $itemshash{$key} ||= 0;
4556 $itemshash{$key} ++; # += $item->{quantity};
4558 @items = sort { $a->{description} cmp $b->{description} }
4559 map { my $i = thaw($_);
4560 $i->{quantity} = $itemshash{$_};
4562 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4568 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4575 return 0 unless $a->itemdesc cmp $b->itemdesc;
4576 return -1 if $b->itemdesc eq 'Tax';
4577 return 1 if $a->itemdesc eq 'Tax';
4578 return -1 if $b->itemdesc eq 'Other surcharges';
4579 return 1 if $a->itemdesc eq 'Other surcharges';
4580 $a->itemdesc cmp $b->itemdesc;
4585 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4586 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4589 sub _items_cust_bill_pkg {
4591 my $cust_bill_pkgs = shift;
4594 my $format = $opt{format} || '';
4595 my $escape_function = $opt{escape_function} || sub { shift };
4596 my $format_function = $opt{format_function} || '';
4597 my $unsquelched = $opt{unsquelched} || '';
4598 my $section = $opt{section}->{description} if $opt{section};
4599 my $summary_page = $opt{summary_page} || '';
4600 my $multilocation = $opt{multilocation} || '';
4601 my $multisection = $opt{multisection} || '';
4602 my $discount_show_always = 0;
4604 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4607 my ($s, $r, $u) = ( undef, undef, undef );
4608 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4611 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4612 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4615 foreach my $display ( grep { defined($section)
4616 ? $_->section eq $section
4619 #grep { !$_->summary || !$summary_page } # bunk!
4620 grep { !$_->summary || $multisection }
4621 $cust_bill_pkg->cust_bill_pkg_display
4625 warn "$me _items_cust_bill_pkg considering display item $display\n"
4628 my $type = $display->type;
4630 my $desc = $cust_bill_pkg->desc;
4631 $desc = substr($desc, 0, $maxlength). '...'
4632 if $format eq 'latex' && length($desc) > $maxlength;
4634 my %details_opt = ( 'format' => $format,
4635 'escape_function' => $escape_function,
4636 'format_function' => $format_function,
4639 if ( $cust_bill_pkg->pkgnum > 0 ) {
4641 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4644 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4646 if ( (!$type || $type eq 'S')
4647 && ( $cust_bill_pkg->setup != 0
4648 || $cust_bill_pkg->setup_show_zero
4653 warn "$me _items_cust_bill_pkg adding setup\n"
4656 my $description = $desc;
4657 $description .= ' Setup'
4658 if $cust_bill_pkg->recur != 0
4659 || $discount_show_always
4660 || $cust_bill_pkg->recur_show_zero;
4663 unless ( $cust_pkg->part_pkg->hide_svc_detail
4664 || $cust_bill_pkg->hidden )
4667 push @d, map &{$escape_function}($_),
4668 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4669 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4671 if ( $multilocation ) {
4672 my $loc = $cust_pkg->location_label;
4673 $loc = substr($loc, 0, $maxlength). '...'
4674 if $format eq 'latex' && length($loc) > $maxlength;
4675 push @d, &{$escape_function}($loc);
4680 push @d, $cust_bill_pkg->details(%details_opt)
4681 if $cust_bill_pkg->recur == 0;
4683 if ( $cust_bill_pkg->hidden ) {
4684 $s->{amount} += $cust_bill_pkg->setup;
4685 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4686 push @{ $s->{ext_description} }, @d;
4690 description => $description,
4691 #pkgpart => $part_pkg->pkgpart,
4692 pkgnum => $cust_bill_pkg->pkgnum,
4693 amount => $cust_bill_pkg->setup,
4694 unit_amount => $cust_bill_pkg->unitsetup,
4695 quantity => $cust_bill_pkg->quantity,
4696 ext_description => \@d,
4702 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4704 $cust_bill_pkg->recur != 0
4705 || $cust_bill_pkg->setup == 0
4706 || $discount_show_always
4707 || $cust_bill_pkg->recur_show_zero
4712 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4715 my $is_summary = $display->summary;
4716 my $description = ($is_summary && $type && $type eq 'U')
4717 ? "Usage charges" : $desc;
4719 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4720 " - ". time2str($date_format, $cust_bill_pkg->edate).
4722 unless $conf->exists('disable_line_item_date_ranges')
4723 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4727 #at least until cust_bill_pkg has "past" ranges in addition to
4728 #the "future" sdate/edate ones... see #3032
4729 my @dates = ( $self->_date );
4730 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4731 push @dates, $prev->sdate if $prev;
4732 push @dates, undef if !$prev;
4734 unless ( $cust_pkg->part_pkg->hide_svc_detail
4735 || $cust_bill_pkg->itemdesc
4736 || $cust_bill_pkg->hidden
4737 || $is_summary && $type && $type eq 'U' )
4740 warn "$me _items_cust_bill_pkg adding service details\n"
4743 push @d, map &{$escape_function}($_),
4744 $cust_pkg->h_labels_short(@dates, 'I')
4745 #$cust_bill_pkg->edate,
4746 #$cust_bill_pkg->sdate)
4747 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4749 warn "$me _items_cust_bill_pkg done adding service details\n"
4752 if ( $multilocation ) {
4753 my $loc = $cust_pkg->location_label;
4754 $loc = substr($loc, 0, $maxlength). '...'
4755 if $format eq 'latex' && length($loc) > $maxlength;
4756 push @d, &{$escape_function}($loc);
4761 unless ( $is_summary ) {
4762 warn "$me _items_cust_bill_pkg adding details\n"
4765 #instead of omitting details entirely in this case (unwanted side
4766 # effects), just omit CDRs
4767 $details_opt{'format_function'} = sub { () }
4768 if $type && $type eq 'R';
4770 push @d, $cust_bill_pkg->details(%details_opt);
4773 warn "$me _items_cust_bill_pkg calculating amount\n"
4778 $amount = $cust_bill_pkg->recur;
4779 } elsif ($type eq 'R') {
4780 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4781 } elsif ($type eq 'U') {
4782 $amount = $cust_bill_pkg->usage;
4785 if ( !$type || $type eq 'R' ) {
4787 warn "$me _items_cust_bill_pkg adding recur\n"
4790 if ( $cust_bill_pkg->hidden ) {
4791 $r->{amount} += $amount;
4792 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4793 push @{ $r->{ext_description} }, @d;
4796 description => $description,
4797 #pkgpart => $part_pkg->pkgpart,
4798 pkgnum => $cust_bill_pkg->pkgnum,
4800 unit_amount => $cust_bill_pkg->unitrecur,
4801 quantity => $cust_bill_pkg->quantity,
4802 ext_description => \@d,
4806 } else { # $type eq 'U'
4808 warn "$me _items_cust_bill_pkg adding usage\n"
4811 if ( $cust_bill_pkg->hidden ) {
4812 $u->{amount} += $amount;
4813 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4814 push @{ $u->{ext_description} }, @d;
4817 description => $description,
4818 #pkgpart => $part_pkg->pkgpart,
4819 pkgnum => $cust_bill_pkg->pkgnum,
4821 unit_amount => $cust_bill_pkg->unitrecur,
4822 quantity => $cust_bill_pkg->quantity,
4823 ext_description => \@d,
4828 } # recurring or usage with recurring charge
4830 } else { #pkgnum tax or one-shot line item (??)
4832 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4835 if ( $cust_bill_pkg->setup != 0 ) {
4837 'description' => $desc,
4838 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4841 if ( $cust_bill_pkg->recur != 0 ) {
4843 'description' => "$desc (".
4844 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4845 time2str($date_format, $cust_bill_pkg->edate). ')',
4846 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4854 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4855 && $conf->exists('discount-show-always'));
4857 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4858 if ( $_ && !$cust_bill_pkg->hidden ) {
4859 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4860 $_->{amount} =~ s/^\-0\.00$/0.00/;
4861 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4863 if $_->{amount} != 0
4864 || $discount_show_always
4865 || ( ! $_->{_is_setup} && $cust_bill_pkg->recur_show_zero )
4866 || ( $_->{_is_setup} && $cust_bill_pkg->setup_show_zero )
4874 #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4876 # $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4877 # $_->{amount} =~ s/^\-0\.00$/0.00/;
4878 # $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4880 # if $_->{amount} != 0
4881 # || $discount_show_always
4885 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4892 sub _items_credits {
4893 my( $self, %opt ) = @_;
4894 my $trim_len = $opt{'trim_len'} || 60;
4898 foreach ( $self->cust_credited ) {
4900 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4902 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4903 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4904 $reason = " ($reason) " if $reason;
4907 #'description' => 'Credit ref\#'. $_->crednum.
4908 # " (". time2str("%x",$_->cust_credit->_date) .")".
4910 'description' => 'Credit applied '.
4911 time2str($date_format,$_->cust_credit->_date). $reason,
4912 'amount' => sprintf("%.2f",$_->amount),
4920 sub _items_payments {
4924 #get & print payments
4925 foreach ( $self->cust_bill_pay ) {
4927 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4930 'description' => "Payment received ".
4931 time2str($date_format,$_->cust_pay->_date ),
4932 'amount' => sprintf("%.2f", $_->amount )
4940 =item call_details [ OPTION => VALUE ... ]
4942 Returns an array of CSV strings representing the call details for this invoice
4943 The only option available is the boolean prepend_billed_number
4948 my ($self, %opt) = @_;
4950 my $format_function = sub { shift };
4952 if ($opt{prepend_billed_number}) {
4953 $format_function = sub {
4957 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4962 my @details = map { $_->details( 'format_function' => $format_function,
4963 'escape_function' => sub{ return() },
4967 $self->cust_bill_pkg;
4968 my $header = $details[0];
4969 ( $header, grep { $_ ne $header } @details );
4979 =item process_reprint
4983 sub process_reprint {
4984 process_re_X('print', @_);
4987 =item process_reemail
4991 sub process_reemail {
4992 process_re_X('email', @_);
5000 process_re_X('fax', @_);
5008 process_re_X('ftp', @_);
5015 sub process_respool {
5016 process_re_X('spool', @_);
5019 use Storable qw(thaw);
5023 my( $method, $job ) = ( shift, shift );
5024 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5026 my $param = thaw(decode_base64(shift));
5027 warn Dumper($param) if $DEBUG;
5038 my($method, $job, %param ) = @_;
5040 warn "re_X $method for job $job with param:\n".
5041 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5044 #some false laziness w/search/cust_bill.html
5046 my $orderby = 'ORDER BY cust_bill._date';
5048 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5050 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5052 my @cust_bill = qsearch( {
5053 #'select' => "cust_bill.*",
5054 'table' => 'cust_bill',
5055 'addl_from' => $addl_from,
5057 'extra_sql' => $extra_sql,
5058 'order_by' => $orderby,
5062 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5064 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5067 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5068 foreach my $cust_bill ( @cust_bill ) {
5069 $cust_bill->$method();
5071 if ( $job ) { #progressbar foo
5073 if ( time - $min_sec > $last ) {
5074 my $error = $job->update_statustext(
5075 int( 100 * $num / scalar(@cust_bill) )
5077 die $error if $error;
5088 =head1 CLASS METHODS
5094 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5099 my ($class, $start, $end) = @_;
5101 $class->paid_sql($start, $end). ' - '.
5102 $class->credited_sql($start, $end);
5107 Returns an SQL fragment to retreive the net amount (charged minus credited).
5112 my ($class, $start, $end) = @_;
5113 'charged - '. $class->credited_sql($start, $end);
5118 Returns an SQL fragment to retreive the amount paid against this invoice.
5123 my ($class, $start, $end) = @_;
5124 $start &&= "AND cust_bill_pay._date <= $start";
5125 $end &&= "AND cust_bill_pay._date > $end";
5126 $start = '' unless defined($start);
5127 $end = '' unless defined($end);
5128 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5129 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5134 Returns an SQL fragment to retreive the amount credited against this invoice.
5139 my ($class, $start, $end) = @_;
5140 $start &&= "AND cust_credit_bill._date <= $start";
5141 $end &&= "AND cust_credit_bill._date > $end";
5142 $start = '' unless defined($start);
5143 $end = '' unless defined($end);
5144 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5145 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5150 Returns an SQL fragment to retrieve the due date of an invoice.
5151 Currently only supported on PostgreSQL.
5159 cust_bill.invoice_terms,
5160 cust_main.invoice_terms,
5161 \''.($conf->config('invoice_default_terms') || '').'\'
5162 ), E\'Net (\\\\d+)\'
5164 ) * 86400 + cust_bill._date'
5167 =item search_sql_where HASHREF
5169 Class method which returns an SQL WHERE fragment to search for parameters
5170 specified in HASHREF. Valid parameters are
5176 List reference of start date, end date, as UNIX timestamps.
5186 List reference of charged limits (exclusive).
5190 List reference of charged limits (exclusive).
5194 flag, return open invoices only
5198 flag, return net invoices only
5202 =item newest_percust
5206 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5210 sub search_sql_where {
5211 my($class, $param) = @_;
5213 warn "$me search_sql_where called with params: \n".
5214 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5220 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5221 push @search, "cust_main.agentnum = $1";
5225 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5226 push @search, "cust_bill.custnum = $1";
5230 if ( $param->{_date} ) {
5231 my($beginning, $ending) = @{$param->{_date}};
5233 push @search, "cust_bill._date >= $beginning",
5234 "cust_bill._date < $ending";
5238 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5239 push @search, "cust_bill.invnum >= $1";
5241 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5242 push @search, "cust_bill.invnum <= $1";
5246 if ( $param->{charged} ) {
5247 my @charged = ref($param->{charged})
5248 ? @{ $param->{charged} }
5249 : ($param->{charged});
5251 push @search, map { s/^charged/cust_bill.charged/; $_; }
5255 my $owed_sql = FS::cust_bill->owed_sql;
5258 if ( $param->{owed} ) {
5259 my @owed = ref($param->{owed})
5260 ? @{ $param->{owed} }
5262 push @search, map { s/^owed/$owed_sql/; $_; }
5267 push @search, "0 != $owed_sql"
5268 if $param->{'open'};
5269 push @search, '0 != '. FS::cust_bill->net_sql
5273 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5274 if $param->{'days'};
5277 if ( $param->{'newest_percust'} ) {
5279 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5280 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5282 my @newest_where = map { my $x = $_;
5283 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5286 grep ! /^cust_main./, @search;
5287 my $newest_where = scalar(@newest_where)
5288 ? ' AND '. join(' AND ', @newest_where)
5292 push @search, "cust_bill._date = (
5293 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5294 WHERE newest_cust_bill.custnum = cust_bill.custnum
5300 #agent virtualization
5301 my $curuser = $FS::CurrentUser::CurrentUser;
5302 if ( $curuser->username eq 'fs_queue'
5303 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5305 my $newuser = qsearchs('access_user', {
5306 'username' => $username,
5310 $curuser = $newuser;
5312 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5315 push @search, $curuser->agentnums_sql;
5317 join(' AND ', @search );
5329 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5330 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base