4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
9 use List::Util qw(min max);
11 use Text::Template 1.20;
13 use String::ShellQuote;
16 use Storable qw( freeze thaw );
18 use FS::UID qw( datasrc );
19 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
20 use FS::Record qw( qsearch qsearchs dbh );
21 use FS::cust_main_Mixin;
23 use FS::cust_statement;
24 use FS::cust_bill_pkg;
25 use FS::cust_bill_pkg_display;
26 use FS::cust_bill_pkg_detail;
30 use FS::cust_credit_bill;
32 use FS::cust_pay_batch;
33 use FS::cust_bill_event;
36 use FS::cust_bill_pay;
37 use FS::cust_bill_pay_batch;
38 use FS::part_bill_event;
41 use FS::cust_bill_batch;
42 use FS::cust_bill_pay_pkg;
43 use FS::cust_credit_bill_pkg;
45 @ISA = qw( FS::cust_main_Mixin FS::Record );
48 $me = '[FS::cust_bill]';
50 #ask FS::UID to run this stuff for us later
51 FS::UID->install_callback( sub {
53 $money_char = $conf->config('money_char') || '$';
54 $date_format = $conf->config('date_format') || '%x'; #/YY
55 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
56 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
61 FS::cust_bill - Object methods for cust_bill records
67 $record = new FS::cust_bill \%hash;
68 $record = new FS::cust_bill { 'column' => 'value' };
70 $error = $record->insert;
72 $error = $new_record->replace($old_record);
74 $error = $record->delete;
76 $error = $record->check;
78 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
80 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
82 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
84 @cust_pay_objects = $cust_bill->cust_pay;
86 $tax_amount = $record->tax;
88 @lines = $cust_bill->print_text;
89 @lines = $cust_bill->print_text $time;
93 An FS::cust_bill object represents an invoice; a declaration that a customer
94 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
95 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
96 following fields are currently supported:
102 =item invnum - primary key (assigned automatically for new invoices)
104 =item custnum - customer (see L<FS::cust_main>)
106 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
107 L<Time::Local> and L<Date::Parse> for conversion functions.
109 =item charged - amount of this invoice
111 =item invoice_terms - optional terms override for this specific invoice
115 Customer info at invoice generation time
119 =item previous_balance
121 =item billing_balance
129 =item printed - deprecated
137 =item closed - books closed flag, empty or `Y'
139 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
141 =item agent_invid - legacy invoice number
151 Creates a new invoice. To add the invoice to the database, see L<"insert">.
152 Invoices are normally created by calling the bill method of a customer object
153 (see L<FS::cust_main>).
157 sub table { 'cust_bill'; }
159 sub cust_linked { $_[0]->cust_main_custnum; }
160 sub cust_unlinked_msg {
162 "WARNING: can't find cust_main.custnum ". $self->custnum.
163 ' (cust_bill.invnum '. $self->invnum. ')';
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
175 warn "$me insert called\n" if $DEBUG;
177 local $SIG{HUP} = 'IGNORE';
178 local $SIG{INT} = 'IGNORE';
179 local $SIG{QUIT} = 'IGNORE';
180 local $SIG{TERM} = 'IGNORE';
181 local $SIG{TSTP} = 'IGNORE';
182 local $SIG{PIPE} = 'IGNORE';
184 my $oldAutoCommit = $FS::UID::AutoCommit;
185 local $FS::UID::AutoCommit = 0;
188 my $error = $self->SUPER::insert;
190 $dbh->rollback if $oldAutoCommit;
194 if ( $self->get('cust_bill_pkg') ) {
195 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
196 $cust_bill_pkg->invnum($self->invnum);
197 my $error = $cust_bill_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "can't create invoice line item: $error";
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212 This method now works but you probably shouldn't use it. Instead, apply a
213 credit against the invoice.
215 Using this method to delete invoices outright is really, really bad. There
216 would be no record you ever posted this invoice, and there are no check to
217 make sure charged = 0 or that there are no associated cust_bill_pkg records.
219 Really, don't use it.
225 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
227 local $SIG{HUP} = 'IGNORE';
228 local $SIG{INT} = 'IGNORE';
229 local $SIG{QUIT} = 'IGNORE';
230 local $SIG{TERM} = 'IGNORE';
231 local $SIG{TSTP} = 'IGNORE';
232 local $SIG{PIPE} = 'IGNORE';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
238 foreach my $table (qw(
251 foreach my $linked ( $self->$table() ) {
252 my $error = $linked->delete;
254 $dbh->rollback if $oldAutoCommit;
261 my $error = $self->SUPER::delete(@_);
263 $dbh->rollback if $oldAutoCommit;
267 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
273 =item replace [ OLD_RECORD ]
275 You can, but probably shouldn't modify invoices...
277 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
278 supplied, replaces this record. If there is an error, returns the error,
279 otherwise returns false.
283 #replace can be inherited from Record.pm
285 # replace_check is now the preferred way to #implement replace data checks
286 # (so $object->replace() works without an argument)
289 my( $new, $old ) = ( shift, shift );
290 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
291 #return "Can't change _date!" unless $old->_date eq $new->_date;
292 return "Can't change _date" unless $old->_date == $new->_date;
293 return "Can't change charged" unless $old->charged == $new->charged
294 || $old->charged == 0
295 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
301 =item add_cc_surcharge
307 sub add_cc_surcharge {
308 my ($self, $pkgnum, $amount) = (shift, shift, shift);
311 my $cust_bill_pkg = new FS::cust_bill_pkg({
312 'invnum' => $self->invnum,
316 $error = $cust_bill_pkg->insert;
317 return $error if $error;
319 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
320 $self->charged($self->charged+$amount);
321 $error = $self->replace;
322 return $error if $error;
324 $self->apply_payments_and_credits;
330 Checks all fields to make sure this is a valid invoice. If there is an error,
331 returns the error, otherwise returns false. Called by the insert and replace
340 $self->ut_numbern('invnum')
341 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
342 || $self->ut_numbern('_date')
343 || $self->ut_money('charged')
344 || $self->ut_numbern('printed')
345 || $self->ut_enum('closed', [ '', 'Y' ])
346 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
347 || $self->ut_numbern('agent_invid') #varchar?
349 return $error if $error;
351 $self->_date(time) unless $self->_date;
353 $self->printed(0) if $self->printed eq '';
360 Returns the displayed invoice number for this invoice: agent_invid if
361 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
367 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
368 return $self->agent_invid;
370 return $self->invnum;
376 Returns a list consisting of the total previous balance for this customer,
377 followed by the previous outstanding invoices (as FS::cust_bill objects also).
384 my @cust_bill = sort { $a->_date <=> $b->_date }
385 grep { $_->owed != 0 && $_->_date < $self->_date }
386 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
388 foreach ( @cust_bill ) { $total += $_->owed; }
394 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
401 { 'table' => 'cust_bill_pkg',
402 'hashref' => { 'invnum' => $self->invnum },
403 'order_by' => 'ORDER BY billpkgnum',
408 =item cust_bill_pkg_pkgnum PKGNUM
410 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
415 sub cust_bill_pkg_pkgnum {
416 my( $self, $pkgnum ) = @_;
418 { 'table' => 'cust_bill_pkg',
419 'hashref' => { 'invnum' => $self->invnum,
422 'order_by' => 'ORDER BY billpkgnum',
429 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
436 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
437 $self->cust_bill_pkg;
439 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
444 Returns true if any of the packages (or their definitions) corresponding to the
445 line items for this invoice have the no_auto flag set.
451 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
454 =item open_cust_bill_pkg
456 Returns the open line items for this invoice.
458 Note that cust_bill_pkg with both setup and recur fees are returned as two
459 separate line items, each with only one fee.
463 # modeled after cust_main::open_cust_bill
464 sub open_cust_bill_pkg {
467 # grep { $_->owed > 0 } $self->cust_bill_pkg
469 my %other = ( 'recur' => 'setup',
470 'setup' => 'recur', );
472 foreach my $field ( qw( recur setup )) {
473 push @open, map { $_->set( $other{$field}, 0 ); $_; }
474 grep { $_->owed($field) > 0 }
475 $self->cust_bill_pkg;
481 =item cust_bill_event
483 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
487 sub cust_bill_event {
489 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
492 =item num_cust_bill_event
494 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
498 sub num_cust_bill_event {
501 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
502 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
503 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
504 $sth->fetchrow_arrayref->[0];
509 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
513 #false laziness w/cust_pkg.pm
517 'table' => 'cust_event',
518 'addl_from' => 'JOIN part_event USING ( eventpart )',
519 'hashref' => { 'tablenum' => $self->invnum },
520 'extra_sql' => " AND eventtable = 'cust_bill' ",
526 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
530 #false laziness w/cust_pkg.pm
534 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
535 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
536 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
537 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
538 $sth->fetchrow_arrayref->[0];
543 Returns the customer (see L<FS::cust_main>) for this invoice.
549 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
552 =item cust_suspend_if_balance_over AMOUNT
554 Suspends the customer associated with this invoice if the total amount owed on
555 this invoice and all older invoices is greater than the specified amount.
557 Returns a list: an empty list on success or a list of errors.
561 sub cust_suspend_if_balance_over {
562 my( $self, $amount ) = ( shift, shift );
563 my $cust_main = $self->cust_main;
564 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
567 $cust_main->suspend(@_);
573 Depreciated. See the cust_credited method.
575 #Returns a list consisting of the total previous credited (see
576 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
577 #outstanding credits (FS::cust_credit objects).
583 croak "FS::cust_bill->cust_credit depreciated; see ".
584 "FS::cust_bill->cust_credit_bill";
587 #my @cust_credit = sort { $a->_date <=> $b->_date }
588 # grep { $_->credited != 0 && $_->_date < $self->_date }
589 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
591 #foreach (@cust_credit) { $total += $_->credited; }
592 #$total, @cust_credit;
597 Depreciated. See the cust_bill_pay method.
599 #Returns all payments (see L<FS::cust_pay>) for this invoice.
605 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
607 #sort { $a->_date <=> $b->_date }
608 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
614 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
617 sub cust_bill_pay_batch {
619 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
624 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
630 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
631 sort { $a->_date <=> $b->_date }
632 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
637 =item cust_credit_bill
639 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
645 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
646 sort { $a->_date <=> $b->_date }
647 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
651 sub cust_credit_bill {
652 shift->cust_credited(@_);
655 #=item cust_bill_pay_pkgnum PKGNUM
657 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
658 #with matching pkgnum.
662 #sub cust_bill_pay_pkgnum {
663 # my( $self, $pkgnum ) = @_;
664 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
665 # sort { $a->_date <=> $b->_date }
666 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
667 # 'pkgnum' => $pkgnum,
672 =item cust_bill_pay_pkg PKGNUM
674 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
675 applied against the matching pkgnum.
679 sub cust_bill_pay_pkg {
680 my( $self, $pkgnum ) = @_;
683 'select' => 'cust_bill_pay_pkg.*',
684 'table' => 'cust_bill_pay_pkg',
685 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
686 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
687 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
688 " AND cust_bill_pkg.pkgnum = $pkgnum",
693 #=item cust_credited_pkgnum PKGNUM
695 #=item cust_credit_bill_pkgnum PKGNUM
697 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
698 #with matching pkgnum.
702 #sub cust_credited_pkgnum {
703 # my( $self, $pkgnum ) = @_;
704 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
705 # sort { $a->_date <=> $b->_date }
706 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
707 # 'pkgnum' => $pkgnum,
712 #sub cust_credit_bill_pkgnum {
713 # shift->cust_credited_pkgnum(@_);
716 =item cust_credit_bill_pkg PKGNUM
718 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
719 applied against the matching pkgnum.
723 sub cust_credit_bill_pkg {
724 my( $self, $pkgnum ) = @_;
727 'select' => 'cust_credit_bill_pkg.*',
728 'table' => 'cust_credit_bill_pkg',
729 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
730 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
731 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
732 " AND cust_bill_pkg.pkgnum = $pkgnum",
737 =item cust_bill_batch
739 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
743 sub cust_bill_batch {
745 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
750 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
757 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
759 foreach (@taxlines) { $total += $_->setup; }
765 Returns the amount owed (still outstanding) on this invoice, which is charged
766 minus all payment applications (see L<FS::cust_bill_pay>) and credit
767 applications (see L<FS::cust_credit_bill>).
773 my $balance = $self->charged;
774 $balance -= $_->amount foreach ( $self->cust_bill_pay );
775 $balance -= $_->amount foreach ( $self->cust_credited );
776 $balance = sprintf( "%.2f", $balance);
777 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
782 my( $self, $pkgnum ) = @_;
784 #my $balance = $self->charged;
786 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
788 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
789 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
791 $balance = sprintf( "%.2f", $balance);
792 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
796 =item apply_payments_and_credits [ OPTION => VALUE ... ]
798 Applies unapplied payments and credits to this invoice.
800 A hash of optional arguments may be passed. Currently "manual" is supported.
801 If true, a payment receipt is sent instead of a statement when
802 'payment_receipt_email' configuration option is set.
804 If there is an error, returns the error, otherwise returns false.
808 sub apply_payments_and_credits {
809 my( $self, %options ) = @_;
811 local $SIG{HUP} = 'IGNORE';
812 local $SIG{INT} = 'IGNORE';
813 local $SIG{QUIT} = 'IGNORE';
814 local $SIG{TERM} = 'IGNORE';
815 local $SIG{TSTP} = 'IGNORE';
816 local $SIG{PIPE} = 'IGNORE';
818 my $oldAutoCommit = $FS::UID::AutoCommit;
819 local $FS::UID::AutoCommit = 0;
822 $self->select_for_update; #mutex
824 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
825 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
827 if ( $conf->exists('pkg-balances') ) {
828 # limit @payments & @credits to those w/ a pkgnum grepped from $self
829 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
830 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
831 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
834 while ( $self->owed > 0 and ( @payments || @credits ) ) {
837 if ( @payments && @credits ) {
839 #decide which goes first by weight of top (unapplied) line item
841 my @open_lineitems = $self->open_cust_bill_pkg;
844 max( map { $_->part_pkg->pay_weight || 0 }
849 my $max_credit_weight =
850 max( map { $_->part_pkg->credit_weight || 0 }
856 #if both are the same... payments first? it has to be something
857 if ( $max_pay_weight >= $max_credit_weight ) {
863 } elsif ( @payments ) {
865 } elsif ( @credits ) {
868 die "guru meditation #12 and 35";
872 if ( $app eq 'pay' ) {
874 my $payment = shift @payments;
875 $unapp_amount = $payment->unapplied;
876 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
877 $app->pkgnum( $payment->pkgnum )
878 if $conf->exists('pkg-balances') && $payment->pkgnum;
880 } elsif ( $app eq 'credit' ) {
882 my $credit = shift @credits;
883 $unapp_amount = $credit->credited;
884 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
885 $app->pkgnum( $credit->pkgnum )
886 if $conf->exists('pkg-balances') && $credit->pkgnum;
889 die "guru meditation #12 and 35";
893 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
894 warn "owed_pkgnum ". $app->pkgnum;
895 $owed = $self->owed_pkgnum($app->pkgnum);
899 next unless $owed > 0;
901 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
902 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
904 $app->invnum( $self->invnum );
906 my $error = $app->insert(%options);
908 $dbh->rollback if $oldAutoCommit;
909 return "Error inserting ". $app->table. " record: $error";
911 die $error if $error;
915 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
920 =item generate_email OPTION => VALUE ...
928 sender address, required
932 alternate template name, optional
936 text attachment arrayref, optional
940 email subject, optional
944 notice name instead of "Invoice", optional
948 Returns an argument list to be passed to L<FS::Misc::send_email>.
959 my $me = '[FS::cust_bill::generate_email]';
962 'from' => $args{'from'},
963 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
967 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
968 'template' => $args{'template'},
969 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
970 'no_coupon' => $args{'no_coupon'},
973 my $cust_main = $self->cust_main;
975 if (ref($args{'to'}) eq 'ARRAY') {
976 $return{'to'} = $args{'to'};
978 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
979 $cust_main->invoicing_list
983 if ( $conf->exists('invoice_html') ) {
985 warn "$me creating HTML/text multipart message"
988 $return{'nobody'} = 1;
990 my $alternative = build MIME::Entity
991 'Type' => 'multipart/alternative',
992 'Encoding' => '7bit',
993 'Disposition' => 'inline'
997 if ( $conf->exists('invoice_email_pdf')
998 and scalar($conf->config('invoice_email_pdf_note')) ) {
1000 warn "$me using 'invoice_email_pdf_note' in multipart message"
1002 $data = [ map { $_ . "\n" }
1003 $conf->config('invoice_email_pdf_note')
1008 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1010 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1011 $data = $args{'print_text'};
1013 $data = [ $self->print_text(\%opt) ];
1018 $alternative->attach(
1019 'Type' => 'text/plain',
1020 #'Encoding' => 'quoted-printable',
1021 'Encoding' => '7bit',
1023 'Disposition' => 'inline',
1026 $args{'from'} =~ /\@([\w\.\-]+)/;
1027 my $from = $1 || 'example.com';
1028 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1031 my $agentnum = $cust_main->agentnum;
1032 if ( defined($args{'template'}) && length($args{'template'})
1033 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1036 $logo = 'logo_'. $args{'template'}. '.png';
1040 my $image_data = $conf->config_binary( $logo, $agentnum);
1042 my $image = build MIME::Entity
1043 'Type' => 'image/png',
1044 'Encoding' => 'base64',
1045 'Data' => $image_data,
1046 'Filename' => 'logo.png',
1047 'Content-ID' => "<$content_id>",
1051 if($conf->exists('invoice-barcode')){
1052 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1053 $barcode = build MIME::Entity
1054 'Type' => 'image/png',
1055 'Encoding' => 'base64',
1056 'Data' => $self->invoice_barcode(0),
1057 'Filename' => 'barcode.png',
1058 'Content-ID' => "<$barcode_content_id>",
1060 $opt{'barcode_cid'} = $barcode_content_id;
1063 $alternative->attach(
1064 'Type' => 'text/html',
1065 'Encoding' => 'quoted-printable',
1066 'Data' => [ '<html>',
1069 ' '. encode_entities($return{'subject'}),
1072 ' <body bgcolor="#e8e8e8">',
1073 $self->print_html({ 'cid'=>$content_id, %opt }),
1077 'Disposition' => 'inline',
1078 #'Filename' => 'invoice.pdf',
1081 my @otherparts = ();
1082 if ( $cust_main->email_csv_cdr ) {
1084 push @otherparts, build MIME::Entity
1085 'Type' => 'text/csv',
1086 'Encoding' => '7bit',
1087 'Data' => [ map { "$_\n" }
1088 $self->call_details('prepend_billed_number' => 1)
1090 'Disposition' => 'attachment',
1091 'Filename' => 'usage-'. $self->invnum. '.csv',
1096 if ( $conf->exists('invoice_email_pdf') ) {
1101 # multipart/alternative
1107 my $related = build MIME::Entity 'Type' => 'multipart/related',
1108 'Encoding' => '7bit';
1110 #false laziness w/Misc::send_email
1111 $related->head->replace('Content-type',
1112 $related->mime_type.
1113 '; boundary="'. $related->head->multipart_boundary. '"'.
1114 '; type=multipart/alternative'
1117 $related->add_part($alternative);
1119 $related->add_part($image);
1121 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1123 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1127 #no other attachment:
1129 # multipart/alternative
1134 $return{'content-type'} = 'multipart/related';
1135 if($conf->exists('invoice-barcode')){
1136 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1139 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1141 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1142 #$return{'disposition'} = 'inline';
1148 if ( $conf->exists('invoice_email_pdf') ) {
1149 warn "$me creating PDF attachment"
1152 #mime parts arguments a la MIME::Entity->build().
1153 $return{'mimeparts'} = [
1154 { $self->mimebuild_pdf(\%opt) }
1158 if ( $conf->exists('invoice_email_pdf')
1159 and scalar($conf->config('invoice_email_pdf_note')) ) {
1161 warn "$me using 'invoice_email_pdf_note'"
1163 $return{'body'} = [ map { $_ . "\n" }
1164 $conf->config('invoice_email_pdf_note')
1169 warn "$me not using 'invoice_email_pdf_note'"
1171 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1172 $return{'body'} = $args{'print_text'};
1174 $return{'body'} = [ $self->print_text(\%opt) ];
1187 Returns a list suitable for passing to MIME::Entity->build(), representing
1188 this invoice as PDF attachment.
1195 'Type' => 'application/pdf',
1196 'Encoding' => 'base64',
1197 'Data' => [ $self->print_pdf(@_) ],
1198 'Disposition' => 'attachment',
1199 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1203 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1205 Sends this invoice to the destinations configured for this customer: sends
1206 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1208 Options can be passed as a hashref (recommended) or as a list of up to
1209 four values for templatename, agentnum, invoice_from and amount.
1211 I<template>, if specified, is the name of a suffix for alternate invoices.
1213 I<agentnum>, if specified, means that this invoice will only be sent for customers
1214 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1215 single agent) or an arrayref of agentnums.
1217 I<invoice_from>, if specified, overrides the default email invoice From: address.
1219 I<amount>, if specified, only sends the invoice if the total amount owed on this
1220 invoice and all older invoices is greater than the specified amount.
1222 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1226 sub queueable_send {
1229 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1230 or die "invalid invoice number: " . $opt{invnum};
1232 my @args = ( $opt{template}, $opt{agentnum} );
1233 push @args, $opt{invoice_from}
1234 if exists($opt{invoice_from}) && $opt{invoice_from};
1236 my $error = $self->send( @args );
1237 die $error if $error;
1244 my( $template, $invoice_from, $notice_name );
1246 my $balance_over = 0;
1250 $template = $opt->{'template'} || '';
1251 if ( $agentnums = $opt->{'agentnum'} ) {
1252 $agentnums = [ $agentnums ] unless ref($agentnums);
1254 $invoice_from = $opt->{'invoice_from'};
1255 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1256 $notice_name = $opt->{'notice_name'};
1258 $template = scalar(@_) ? shift : '';
1259 if ( scalar(@_) && $_[0] ) {
1260 $agentnums = ref($_[0]) ? shift : [ shift ];
1262 $invoice_from = shift if scalar(@_);
1263 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1266 return 'N/A' unless ! $agentnums
1267 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1270 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1272 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1273 $conf->config('invoice_from', $self->cust_main->agentnum );
1276 'template' => $template,
1277 'invoice_from' => $invoice_from,
1278 'notice_name' => ( $notice_name || 'Invoice' ),
1281 my @invoicing_list = $self->cust_main->invoicing_list;
1283 #$self->email_invoice(\%opt)
1285 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1287 #$self->print_invoice(\%opt)
1289 if grep { $_ eq 'POST' } @invoicing_list; #postal
1291 $self->fax_invoice(\%opt)
1292 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1298 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1300 Emails this invoice.
1302 Options can be passed as a hashref (recommended) or as a list of up to
1303 two values for templatename and invoice_from.
1305 I<template>, if specified, is the name of a suffix for alternate invoices.
1307 I<invoice_from>, if specified, overrides the default email invoice From: address.
1309 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1313 sub queueable_email {
1316 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1317 or die "invalid invoice number: " . $opt{invnum};
1319 my %args = ( 'template' => $opt{template} );
1320 $args{$_} = $opt{$_}
1321 foreach grep { exists($opt{$_}) && $opt{$_} }
1322 qw( invoice_from notice_name no_coupon );
1324 my $error = $self->email( \%args );
1325 die $error if $error;
1329 #sub email_invoice {
1333 my( $template, $invoice_from, $notice_name, $no_coupon );
1336 $template = $opt->{'template'} || '';
1337 $invoice_from = $opt->{'invoice_from'};
1338 $notice_name = $opt->{'notice_name'} || 'Invoice';
1339 $no_coupon = $opt->{'no_coupon'} || 0;
1341 $template = scalar(@_) ? shift : '';
1342 $invoice_from = shift if scalar(@_);
1343 $notice_name = 'Invoice';
1347 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1348 $conf->config('invoice_from', $self->cust_main->agentnum );
1350 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1351 $self->cust_main->invoicing_list;
1353 if ( ! @invoicing_list ) { #no recipients
1354 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1355 die 'No recipients for customer #'. $self->custnum;
1357 #default: better to notify this person than silence
1358 @invoicing_list = ($invoice_from);
1362 my $subject = $self->email_subject($template);
1364 my $error = send_email(
1365 $self->generate_email(
1366 'from' => $invoice_from,
1367 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1368 'subject' => $subject,
1369 'template' => $template,
1370 'notice_name' => $notice_name,
1371 'no_coupon' => $no_coupon,
1374 die "can't email invoice: $error\n" if $error;
1375 #die "$error\n" if $error;
1382 #my $template = scalar(@_) ? shift : '';
1385 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1388 my $cust_main = $self->cust_main;
1389 my $name = $cust_main->name;
1390 my $name_short = $cust_main->name_short;
1391 my $invoice_number = $self->invnum;
1392 my $invoice_date = $self->_date_pretty;
1394 eval qq("$subject");
1397 =item lpr_data HASHREF | [ TEMPLATE ]
1399 Returns the postscript or plaintext for this invoice as an arrayref.
1401 Options can be passed as a hashref (recommended) or as a single optional value
1404 I<template>, if specified, is the name of a suffix for alternate invoices.
1406 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1412 my( $template, $notice_name );
1415 $template = $opt->{'template'} || '';
1416 $notice_name = $opt->{'notice_name'} || 'Invoice';
1418 $template = scalar(@_) ? shift : '';
1419 $notice_name = 'Invoice';
1423 'template' => $template,
1424 'notice_name' => $notice_name,
1427 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1428 [ $self->$method( \%opt ) ];
1431 =item print HASHREF | [ TEMPLATE ]
1433 Prints this invoice.
1435 Options can be passed as a hashref (recommended) or as a single optional
1438 I<template>, if specified, is the name of a suffix for alternate invoices.
1440 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1444 #sub print_invoice {
1447 my( $template, $notice_name );
1450 $template = $opt->{'template'} || '';
1451 $notice_name = $opt->{'notice_name'} || 'Invoice';
1453 $template = scalar(@_) ? shift : '';
1454 $notice_name = 'Invoice';
1458 'template' => $template,
1459 'notice_name' => $notice_name,
1462 if($conf->exists('invoice_print_pdf')) {
1463 # Add the invoice to the current batch.
1464 $self->batch_invoice(\%opt);
1467 do_print $self->lpr_data(\%opt);
1471 =item fax_invoice HASHREF | [ TEMPLATE ]
1475 Options can be passed as a hashref (recommended) or as a single optional
1478 I<template>, if specified, is the name of a suffix for alternate invoices.
1480 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1486 my( $template, $notice_name );
1489 $template = $opt->{'template'} || '';
1490 $notice_name = $opt->{'notice_name'} || 'Invoice';
1492 $template = scalar(@_) ? shift : '';
1493 $notice_name = 'Invoice';
1496 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1497 unless $conf->exists('invoice_latex');
1499 my $dialstring = $self->cust_main->getfield('fax');
1503 'template' => $template,
1504 'notice_name' => $notice_name,
1507 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1508 'dialstring' => $dialstring,
1510 die $error if $error;
1514 =item batch_invoice [ HASHREF ]
1516 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1517 isn't an open batch, one will be created.
1522 my ($self, $opt) = @_;
1523 my $batch = FS::bill_batch->get_open_batch;
1524 my $cust_bill_batch = FS::cust_bill_batch->new({
1525 batchnum => $batch->batchnum,
1526 invnum => $self->invnum,
1528 return $cust_bill_batch->insert($opt);
1531 =item ftp_invoice [ TEMPLATENAME ]
1533 Sends this invoice data via FTP.
1535 TEMPLATENAME is unused?
1541 my $template = scalar(@_) ? shift : '';
1544 'protocol' => 'ftp',
1545 'server' => $conf->config('cust_bill-ftpserver'),
1546 'username' => $conf->config('cust_bill-ftpusername'),
1547 'password' => $conf->config('cust_bill-ftppassword'),
1548 'dir' => $conf->config('cust_bill-ftpdir'),
1549 'format' => $conf->config('cust_bill-ftpformat'),
1553 =item spool_invoice [ TEMPLATENAME ]
1555 Spools this invoice data (see L<FS::spool_csv>)
1557 TEMPLATENAME is unused?
1563 my $template = scalar(@_) ? shift : '';
1566 'format' => $conf->config('cust_bill-spoolformat'),
1567 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1571 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1573 Like B<send>, but only sends the invoice if it is the newest open invoice for
1578 sub send_if_newest {
1583 grep { $_->owed > 0 }
1584 qsearch('cust_bill', {
1585 'custnum' => $self->custnum,
1586 #'_date' => { op=>'>', value=>$self->_date },
1587 'invnum' => { op=>'>', value=>$self->invnum },
1594 =item send_csv OPTION => VALUE, ...
1596 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1600 protocol - currently only "ftp"
1606 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1607 and YYMMDDHHMMSS is a timestamp.
1609 See L</print_csv> for a description of the output format.
1614 my($self, %opt) = @_;
1618 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1619 mkdir $spooldir, 0700 unless -d $spooldir;
1621 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1622 my $file = "$spooldir/$tracctnum.csv";
1624 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1626 open(CSV, ">$file") or die "can't open $file: $!";
1634 if ( $opt{protocol} eq 'ftp' ) {
1635 eval "use Net::FTP;";
1637 $net = Net::FTP->new($opt{server}) or die @$;
1639 die "unknown protocol: $opt{protocol}";
1642 $net->login( $opt{username}, $opt{password} )
1643 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1645 $net->binary or die "can't set binary mode";
1647 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1649 $net->put($file) or die "can't put $file: $!";
1659 Spools CSV invoice data.
1665 =item format - 'default' or 'billco'
1667 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1669 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1671 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1678 my($self, %opt) = @_;
1680 my $cust_main = $self->cust_main;
1682 if ( $opt{'dest'} ) {
1683 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1684 $cust_main->invoicing_list;
1685 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1686 || ! keys %invoicing_list;
1689 if ( $opt{'balanceover'} ) {
1691 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1694 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1695 mkdir $spooldir, 0700 unless -d $spooldir;
1697 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1701 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1702 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1705 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1707 open(CSV, ">>$file") or die "can't open $file: $!";
1708 flock(CSV, LOCK_EX);
1713 if ( lc($opt{'format'}) eq 'billco' ) {
1715 flock(CSV, LOCK_UN);
1720 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1723 open(CSV,">>$file") or die "can't open $file: $!";
1724 flock(CSV, LOCK_EX);
1730 flock(CSV, LOCK_UN);
1737 =item print_csv OPTION => VALUE, ...
1739 Returns CSV data for this invoice.
1743 format - 'default' or 'billco'
1745 Returns a list consisting of two scalars. The first is a single line of CSV
1746 header information for this invoice. The second is one or more lines of CSV
1747 detail information for this invoice.
1749 If I<format> is not specified or "default", the fields of the CSV file are as
1752 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1756 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1758 B<record_type> is C<cust_bill> for the initial header line only. The
1759 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1760 fields are filled in.
1762 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1763 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1766 =item invnum - invoice number
1768 =item custnum - customer number
1770 =item _date - invoice date
1772 =item charged - total invoice amount
1774 =item first - customer first name
1776 =item last - customer first name
1778 =item company - company name
1780 =item address1 - address line 1
1782 =item address2 - address line 1
1792 =item pkg - line item description
1794 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1796 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1798 =item sdate - start date for recurring fee
1800 =item edate - end date for recurring fee
1804 If I<format> is "billco", the fields of the header CSV file are as follows:
1806 +-------------------------------------------------------------------+
1807 | FORMAT HEADER FILE |
1808 |-------------------------------------------------------------------|
1809 | Field | Description | Name | Type | Width |
1810 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1811 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1812 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1813 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1814 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1815 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1816 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1817 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1818 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1819 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1820 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1821 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1822 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1823 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1824 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1825 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1826 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1827 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1828 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1829 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1830 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1831 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1832 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1833 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1834 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1835 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1836 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1837 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1838 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1839 +-------+-------------------------------+------------+------+-------+
1841 If I<format> is "billco", the fields of the detail CSV file are as follows:
1843 FORMAT FOR DETAIL FILE
1845 Field | Description | Name | Type | Width
1846 1 | N/A-Leave Empty | RC | CHAR | 2
1847 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1848 3 | Account Number | TRACCTNUM | CHAR | 15
1849 4 | Invoice Number | TRINVOICE | CHAR | 15
1850 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1851 6 | Transaction Detail | DETAILS | CHAR | 100
1852 7 | Amount | AMT | NUM* | 9
1853 8 | Line Format Control** | LNCTRL | CHAR | 2
1854 9 | Grouping Code | GROUP | CHAR | 2
1855 10 | User Defined | ACCT CODE | CHAR | 15
1860 my($self, %opt) = @_;
1862 eval "use Text::CSV_XS";
1865 my $cust_main = $self->cust_main;
1867 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1869 if ( lc($opt{'format'}) eq 'billco' ) {
1872 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1874 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1876 my( $previous_balance, @unused ) = $self->previous; #previous balance
1878 my $pmt_cr_applied = 0;
1879 $pmt_cr_applied += $_->{'amount'}
1880 foreach ( $self->_items_payments, $self->_items_credits ) ;
1882 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1885 '', # 1 | N/A-Leave Empty CHAR 2
1886 '', # 2 | N/A-Leave Empty CHAR 15
1887 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1888 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1889 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1890 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1891 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1892 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1893 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1894 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1895 '', # 10 | Ancillary Billing Information CHAR 30
1896 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1897 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1900 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1903 $duedate, # 14 | Bill Due Date CHAR 10
1905 $previous_balance, # 15 | Previous Balance NUM* 9
1906 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1907 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1908 $totaldue, # 18 | Total Amt Due NUM* 9
1909 $totaldue, # 19 | Total Amt Due NUM* 9
1910 '', # 20 | 30 Day Aging NUM* 9
1911 '', # 21 | 60 Day Aging NUM* 9
1912 '', # 22 | 90 Day Aging NUM* 9
1913 'N', # 23 | Y/N CHAR 1
1914 '', # 24 | Remittance automation CHAR 100
1915 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1916 $self->custnum, # 26 | Customer Reference Number CHAR 15
1917 '0', # 27 | Federal Tax*** NUM* 9
1918 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1919 '0', # 29 | Other Taxes & Fees*** NUM* 9
1928 time2str("%x", $self->_date),
1929 sprintf("%.2f", $self->charged),
1930 ( map { $cust_main->getfield($_) }
1931 qw( first last company address1 address2 city state zip country ) ),
1933 ) or die "can't create csv";
1936 my $header = $csv->string. "\n";
1939 if ( lc($opt{'format'}) eq 'billco' ) {
1942 foreach my $item ( $self->_items_pkg ) {
1945 '', # 1 | N/A-Leave Empty CHAR 2
1946 '', # 2 | N/A-Leave Empty CHAR 15
1947 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1948 $self->invnum, # 4 | Invoice Number CHAR 15
1949 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1950 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1951 $item->{'amount'}, # 7 | Amount NUM* 9
1952 '', # 8 | Line Format Control** CHAR 2
1953 '', # 9 | Grouping Code CHAR 2
1954 '', # 10 | User Defined CHAR 15
1957 $detail .= $csv->string. "\n";
1963 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1965 my($pkg, $setup, $recur, $sdate, $edate);
1966 if ( $cust_bill_pkg->pkgnum ) {
1968 ($pkg, $setup, $recur, $sdate, $edate) = (
1969 $cust_bill_pkg->part_pkg->pkg,
1970 ( $cust_bill_pkg->setup != 0
1971 ? sprintf("%.2f", $cust_bill_pkg->setup )
1973 ( $cust_bill_pkg->recur != 0
1974 ? sprintf("%.2f", $cust_bill_pkg->recur )
1976 ( $cust_bill_pkg->sdate
1977 ? time2str("%x", $cust_bill_pkg->sdate)
1979 ($cust_bill_pkg->edate
1980 ?time2str("%x", $cust_bill_pkg->edate)
1984 } else { #pkgnum tax
1985 next unless $cust_bill_pkg->setup != 0;
1986 $pkg = $cust_bill_pkg->desc;
1987 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1988 ( $sdate, $edate ) = ( '', '' );
1994 ( map { '' } (1..11) ),
1995 ($pkg, $setup, $recur, $sdate, $edate)
1996 ) or die "can't create csv";
1998 $detail .= $csv->string. "\n";
2004 ( $header, $detail );
2010 Pays this invoice with a compliemntary payment. If there is an error,
2011 returns the error, otherwise returns false.
2017 my $cust_pay = new FS::cust_pay ( {
2018 'invnum' => $self->invnum,
2019 'paid' => $self->owed,
2022 'payinfo' => $self->cust_main->payinfo,
2030 Attempts to pay this invoice with a credit card payment via a
2031 Business::OnlinePayment realtime gateway. See
2032 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2033 for supported processors.
2039 $self->realtime_bop( 'CC', @_ );
2044 Attempts to pay this invoice with an electronic check (ACH) payment via a
2045 Business::OnlinePayment realtime gateway. See
2046 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2047 for supported processors.
2053 $self->realtime_bop( 'ECHECK', @_ );
2058 Attempts to pay this invoice with phone bill (LEC) payment via a
2059 Business::OnlinePayment realtime gateway. See
2060 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2061 for supported processors.
2067 $self->realtime_bop( 'LEC', @_ );
2071 my( $self, $method ) = (shift,shift);
2074 my $cust_main = $self->cust_main;
2075 my $balance = $cust_main->balance;
2076 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2077 $amount = sprintf("%.2f", $amount);
2078 return "not run (balance $balance)" unless $amount > 0;
2080 my $description = 'Internet Services';
2081 if ( $conf->exists('business-onlinepayment-description') ) {
2082 my $dtempl = $conf->config('business-onlinepayment-description');
2084 my $agent_obj = $cust_main->agent
2085 or die "can't retreive agent for $cust_main (agentnum ".
2086 $cust_main->agentnum. ")";
2087 my $agent = $agent_obj->agent;
2088 my $pkgs = join(', ',
2089 map { $_->part_pkg->pkg }
2090 grep { $_->pkgnum } $self->cust_bill_pkg
2092 $description = eval qq("$dtempl");
2095 $cust_main->realtime_bop($method, $amount,
2096 'description' => $description,
2097 'invnum' => $self->invnum,
2098 #this didn't do what we want, it just calls apply_payments_and_credits
2100 'apply_to_invoice' => 1,
2103 #this changes application behavior: auto payments
2104 #triggered against a specific invoice are now applied
2105 #to that invoice instead of oldest open.
2111 =item batch_card OPTION => VALUE...
2113 Adds a payment for this invoice to the pending credit card batch (see
2114 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2115 runs the payment using a realtime gateway.
2120 my ($self, %options) = @_;
2121 my $cust_main = $self->cust_main;
2123 $options{invnum} = $self->invnum;
2125 $cust_main->batch_card(%options);
2128 sub _agent_template {
2130 $self->cust_main->agent_template;
2133 sub _agent_invoice_from {
2135 $self->cust_main->agent_invoice_from;
2138 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2140 Returns an text invoice, as a list of lines.
2142 Options can be passed as a hashref (recommended) or as a list of time, template
2143 and then any key/value pairs for any other options.
2145 I<time>, if specified, is used to control the printing of overdue messages. The
2146 default is now. It isn't the date of the invoice; that's the `_date' field.
2147 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2148 L<Time::Local> and L<Date::Parse> for conversion functions.
2150 I<template>, if specified, is the name of a suffix for alternate invoices.
2152 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2158 my( $today, $template, %opt );
2160 %opt = %{ shift() };
2161 $today = delete($opt{'time'}) || '';
2162 $template = delete($opt{template}) || '';
2164 ( $today, $template, %opt ) = @_;
2167 my %params = ( 'format' => 'template' );
2168 $params{'time'} = $today if $today;
2169 $params{'template'} = $template if $template;
2170 $params{$_} = $opt{$_}
2171 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2173 $self->print_generic( %params );
2176 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2178 Internal method - returns a filename of a filled-in LaTeX template for this
2179 invoice (Note: add ".tex" to get the actual filename), and a filename of
2180 an associated logo (with the .eps extension included).
2182 See print_ps and print_pdf for methods that return PostScript and PDF output.
2184 Options can be passed as a hashref (recommended) or as a list of time, template
2185 and then any key/value pairs for any other options.
2187 I<time>, if specified, is used to control the printing of overdue messages. The
2188 default is now. It isn't the date of the invoice; that's the `_date' field.
2189 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2190 L<Time::Local> and L<Date::Parse> for conversion functions.
2192 I<template>, if specified, is the name of a suffix for alternate invoices.
2194 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2200 my( $today, $template, %opt );
2202 %opt = %{ shift() };
2203 $today = delete($opt{'time'}) || '';
2204 $template = delete($opt{template}) || '';
2206 ( $today, $template, %opt ) = @_;
2209 my %params = ( 'format' => 'latex' );
2210 $params{'time'} = $today if $today;
2211 $params{'template'} = $template if $template;
2212 $params{$_} = $opt{$_}
2213 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2215 $template ||= $self->_agent_template;
2217 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2218 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2222 ) or die "can't open temp file: $!\n";
2224 my $agentnum = $self->cust_main->agentnum;
2226 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2227 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2228 or die "can't write temp file: $!\n";
2230 print $lh $conf->config_binary('logo.eps', $agentnum)
2231 or die "can't write temp file: $!\n";
2234 $params{'logo_file'} = $lh->filename;
2236 if($conf->exists('invoice-barcode')){
2237 my $png_file = $self->invoice_barcode($dir);
2238 my $eps_file = $png_file;
2239 $eps_file =~ s/\.png$/.eps/g;
2240 $png_file =~ /(barcode.*png)/;
2242 $eps_file =~ /(barcode.*eps)/;
2245 my $curr_dir = cwd();
2247 # after painfuly long experimentation, it was determined that sam2p won't
2248 # accept : and other chars in the path, no matter how hard I tried to
2249 # escape them, hence the chdir (and chdir back, just to be safe)
2250 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2251 or die "sam2p failed: $!\n";
2255 $params{'barcode_file'} = $eps_file;
2258 my @filled_in = $self->print_generic( %params );
2260 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2264 ) or die "can't open temp file: $!\n";
2265 print $fh join('', @filled_in );
2268 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2269 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2273 =item invoice_barcode DIR_OR_FALSE
2275 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2276 it is taken as the temp directory where the PNG file will be generated and the
2277 PNG file name is returned. Otherwise, the PNG image itself is returned.
2281 sub invoice_barcode {
2282 my ($self, $dir) = (shift,shift);
2284 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2285 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2286 my $gd = $gdbar->plot(Height => 30);
2289 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2293 ) or die "can't open temp file: $!\n";
2294 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2295 my $png_file = $bh->filename;
2302 =item print_generic OPTION => VALUE ...
2304 Internal method - returns a filled-in template for this invoice as a scalar.
2306 See print_ps and print_pdf for methods that return PostScript and PDF output.
2308 Non optional options include
2309 format - latex, html, template
2311 Optional options include
2313 template - a value used as a suffix for a configuration template
2315 time - a value used to control the printing of overdue messages. The
2316 default is now. It isn't the date of the invoice; that's the `_date' field.
2317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2318 L<Time::Local> and L<Date::Parse> for conversion functions.
2322 unsquelch_cdr - overrides any per customer cdr squelching when true
2324 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2328 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2329 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2330 # yes: fixed width (dot matrix) text printing will be borked
2333 my( $self, %params ) = @_;
2334 my $today = $params{today} ? $params{today} : time;
2335 warn "$me print_generic called on $self with suffix $params{template}\n"
2338 my $format = $params{format};
2339 die "Unknown format: $format"
2340 unless $format =~ /^(latex|html|template)$/;
2342 my $cust_main = $self->cust_main;
2343 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2344 unless $cust_main->payname
2345 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2347 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2348 'html' => [ '<%=', '%>' ],
2349 'template' => [ '{', '}' ],
2352 warn "$me print_generic creating template\n"
2355 #create the template
2356 my $template = $params{template} ? $params{template} : $self->_agent_template;
2357 my $templatefile = "invoice_$format";
2358 $templatefile .= "_$template"
2359 if length($template) && $conf->exists($templatefile."_$template");
2360 my @invoice_template = map "$_\n", $conf->config($templatefile)
2361 or die "cannot load config data $templatefile";
2364 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2365 #change this to a die when the old code is removed
2366 warn "old-style invoice template $templatefile; ".
2367 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2368 $old_latex = 'true';
2369 @invoice_template = _translate_old_latex_format(@invoice_template);
2372 warn "$me print_generic creating T:T object\n"
2375 my $text_template = new Text::Template(
2377 SOURCE => \@invoice_template,
2378 DELIMITERS => $delimiters{$format},
2381 warn "$me print_generic compiling T:T object\n"
2384 $text_template->compile()
2385 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2388 # additional substitution could possibly cause breakage in existing templates
2389 my %convert_maps = (
2391 'notes' => sub { map "$_", @_ },
2392 'footer' => sub { map "$_", @_ },
2393 'smallfooter' => sub { map "$_", @_ },
2394 'returnaddress' => sub { map "$_", @_ },
2395 'coupon' => sub { map "$_", @_ },
2396 'summary' => sub { map "$_", @_ },
2402 s/%%(.*)$/<!-- $1 -->/g;
2403 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2404 s/\\begin\{enumerate\}/<ol>/g;
2406 s/\\end\{enumerate\}/<\/ol>/g;
2407 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2416 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2418 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2423 s/\\\\\*?\s*$/<BR>/;
2424 s/\\hyphenation\{[\w\s\-]+}//;
2429 'coupon' => sub { "" },
2430 'summary' => sub { "" },
2437 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2438 s/\\begin\{enumerate\}//g;
2440 s/\\end\{enumerate\}//g;
2441 s/\\textbf\{(.*)\}/$1/g;
2448 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2450 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2455 s/\\\\\*?\s*$/\n/; # dubious
2456 s/\\hyphenation\{[\w\s\-]+}//;
2460 'coupon' => sub { "" },
2461 'summary' => sub { "" },
2466 # hashes for differing output formats
2467 my %nbsps = ( 'latex' => '~',
2468 'html' => '', # '&nbps;' would be nice
2469 'template' => '', # not used
2471 my $nbsp = $nbsps{$format};
2473 my %escape_functions = ( 'latex' => \&_latex_escape,
2474 'html' => \&_html_escape_nbsp,#\&encode_entities,
2475 'template' => sub { shift },
2477 my $escape_function = $escape_functions{$format};
2478 my $escape_function_nonbsp = ($format eq 'html')
2479 ? \&_html_escape : $escape_function;
2481 my %date_formats = ( 'latex' => $date_format_long,
2482 'html' => $date_format_long,
2485 $date_formats{'html'} =~ s/ / /g;
2487 my $date_format = $date_formats{$format};
2489 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2491 'html' => sub { return '<b>'. shift(). '</b>'
2493 'template' => sub { shift },
2495 my $embolden_function = $embolden_functions{$format};
2497 my %newline_tokens = ( 'latex' => '\\\\',
2501 my $newline_token = $newline_tokens{$format};
2503 warn "$me generating template variables\n"
2506 # generate template variables
2509 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2513 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2519 $returnaddress = join("\n",
2520 $conf->config_orbase("invoice_${format}returnaddress", $template)
2523 } elsif ( grep /\S/,
2524 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2526 my $convert_map = $convert_maps{$format}{'returnaddress'};
2529 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2534 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2536 my $convert_map = $convert_maps{$format}{'returnaddress'};
2537 $returnaddress = join( "\n", &$convert_map(
2538 map { s/( {2,})/'~' x length($1)/eg;
2542 ( $conf->config('company_name', $self->cust_main->agentnum),
2543 $conf->config('company_address', $self->cust_main->agentnum),
2550 my $warning = "Couldn't find a return address; ".
2551 "do you need to set the company_address configuration value?";
2553 $returnaddress = $nbsp;
2554 #$returnaddress = $warning;
2558 warn "$me generating invoice data\n"
2561 my $agentnum = $self->cust_main->agentnum;
2563 my %invoice_data = (
2566 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2567 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2568 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2569 'returnaddress' => $returnaddress,
2570 'agent' => &$escape_function($cust_main->agent->agent),
2573 'invnum' => $self->invnum,
2574 'date' => time2str($date_format, $self->_date),
2575 'today' => time2str($date_format_long, $today),
2576 'terms' => $self->terms,
2577 'template' => $template, #params{'template'},
2578 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2579 'current_charges' => sprintf("%.2f", $self->charged),
2580 'duedate' => $self->due_date2str($rdate_format), #date_format?
2583 'custnum' => $cust_main->display_custnum,
2584 'agent_custid' => &$escape_function($cust_main->agent_custid),
2585 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2586 payname company address1 address2 city state zip fax
2590 'ship_enable' => $conf->exists('invoice-ship_address'),
2591 'unitprices' => $conf->exists('invoice-unitprice'),
2592 'smallernotes' => $conf->exists('invoice-smallernotes'),
2593 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2594 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2596 #layout info -- would be fancy to calc some of this and bury the template
2598 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2599 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2600 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2601 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2602 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2603 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2604 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2605 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2606 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2607 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2609 # better hang on to conf_dir for a while (for old templates)
2610 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2612 #these are only used when doing paged plaintext
2618 my $min_sdate = 999999999999;
2620 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2621 next unless $cust_bill_pkg->pkgnum > 0;
2622 $min_sdate = $cust_bill_pkg->sdate
2623 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2624 $max_edate = $cust_bill_pkg->edate
2625 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2628 $invoice_data{'bill_period'} = '';
2629 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2630 . " to " . time2str('%e %h', $max_edate)
2631 if ($max_edate != 0 && $min_sdate != 999999999999);
2633 $invoice_data{finance_section} = '';
2634 if ( $conf->config('finance_pkgclass') ) {
2636 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2637 $invoice_data{finance_section} = $pkg_class->categoryname;
2639 $invoice_data{finance_amount} = '0.00';
2640 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2642 my $countrydefault = $conf->config('countrydefault') || 'US';
2643 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2644 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2645 my $method = $prefix.$_;
2646 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2648 $invoice_data{'ship_country'} = ''
2649 if ( $invoice_data{'ship_country'} eq $countrydefault );
2651 $invoice_data{'cid'} = $params{'cid'}
2654 if ( $cust_main->country eq $countrydefault ) {
2655 $invoice_data{'country'} = '';
2657 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2661 $invoice_data{'address'} = \@address;
2663 $cust_main->payname.
2664 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2665 ? " (P.O. #". $cust_main->payinfo. ")"
2669 push @address, $cust_main->company
2670 if $cust_main->company;
2671 push @address, $cust_main->address1;
2672 push @address, $cust_main->address2
2673 if $cust_main->address2;
2675 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2676 push @address, $invoice_data{'country'}
2677 if $invoice_data{'country'};
2679 while (scalar(@address) < 5);
2681 $invoice_data{'logo_file'} = $params{'logo_file'}
2682 if $params{'logo_file'};
2683 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2684 if $params{'barcode_file'};
2685 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2686 if $params{'barcode_img'};
2687 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2688 if $params{'barcode_cid'};
2690 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2691 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2692 #my $balance_due = $self->owed + $pr_total - $cr_total;
2693 my $balance_due = $self->owed + $pr_total;
2694 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2695 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2696 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2697 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2699 my $summarypage = '';
2700 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2703 $invoice_data{'summarypage'} = $summarypage;
2705 warn "$me substituting variables in notes, footer, smallfooter\n"
2708 my @include = (qw( notes footer smallfooter ));
2709 push @include, 'coupon' unless $params{'no_coupon'};
2710 foreach my $include (@include) {
2712 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2715 if ( $conf->exists($inc_file, $agentnum)
2716 && length( $conf->config($inc_file, $agentnum) ) ) {
2718 @inc_src = $conf->config($inc_file, $agentnum);
2722 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2724 my $convert_map = $convert_maps{$format}{$include};
2726 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2727 s/--\@\]/$delimiters{$format}[1]/g;
2730 &$convert_map( $conf->config($inc_file, $agentnum) );
2734 my $inc_tt = new Text::Template (
2736 SOURCE => [ map "$_\n", @inc_src ],
2737 DELIMITERS => $delimiters{$format},
2738 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2740 unless ( $inc_tt->compile() ) {
2741 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2742 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2746 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2748 $invoice_data{$include} =~ s/\n+$//
2749 if ($format eq 'latex');
2752 $invoice_data{'po_line'} =
2753 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2754 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2757 my %money_chars = ( 'latex' => '',
2758 'html' => $conf->config('money_char') || '$',
2761 my $money_char = $money_chars{$format};
2763 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2764 'html' => $conf->config('money_char') || '$',
2767 my $other_money_char = $other_money_chars{$format};
2768 $invoice_data{'dollar'} = $other_money_char;
2770 my @detail_items = ();
2771 my @total_items = ();
2775 $invoice_data{'detail_items'} = \@detail_items;
2776 $invoice_data{'total_items'} = \@total_items;
2777 $invoice_data{'buf'} = \@buf;
2778 $invoice_data{'sections'} = \@sections;
2780 warn "$me generating sections\n"
2783 my $previous_section = { 'description' => 'Previous Charges',
2784 'subtotal' => $other_money_char.
2785 sprintf('%.2f', $pr_total),
2786 'summarized' => $summarypage ? 'Y' : '',
2788 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2789 join(' / ', map { $cust_main->balance_date_range(@$_) }
2790 $self->_prior_month30s
2792 if $conf->exists('invoice_include_aging');
2795 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2796 'subtotal' => $taxtotal, # adjusted below
2797 'summarized' => $summarypage ? 'Y' : '',
2799 my $tax_weight = _pkg_category($tax_section->{description})
2800 ? _pkg_category($tax_section->{description})->weight
2802 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2803 $tax_section->{'sort_weight'} = $tax_weight;
2806 my $adjusttotal = 0;
2807 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2808 'subtotal' => 0, # adjusted below
2809 'summarized' => $summarypage ? 'Y' : '',
2811 my $adjust_weight = _pkg_category($adjust_section->{description})
2812 ? _pkg_category($adjust_section->{description})->weight
2814 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2815 $adjust_section->{'sort_weight'} = $adjust_weight;
2817 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2818 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2819 $invoice_data{'multisection'} = $multisection;
2820 my $late_sections = [];
2821 my $extra_sections = [];
2822 my $extra_lines = ();
2823 if ( $multisection ) {
2824 ($extra_sections, $extra_lines) =
2825 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2826 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2828 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2830 push @detail_items, @$extra_lines if $extra_lines;
2832 $self->_items_sections( $late_sections, # this could stand a refactor
2834 $escape_function_nonbsp,
2838 if ($conf->exists('svc_phone_sections')) {
2839 my ($phone_sections, $phone_lines) =
2840 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2841 push @{$late_sections}, @$phone_sections;
2842 push @detail_items, @$phone_lines;
2844 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2845 my ($accountcode_section, $accountcode_lines) =
2846 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2847 if ( scalar(@$accountcode_lines) ) {
2848 push @{$late_sections}, $accountcode_section;
2849 push @detail_items, @$accountcode_lines;
2853 push @sections, { 'description' => '', 'subtotal' => '' };
2856 unless ( $conf->exists('disable_previous_balance')
2857 || $conf->exists('previous_balance-summary_only')
2861 warn "$me adding previous balances\n"
2864 foreach my $line_item ( $self->_items_previous ) {
2867 ext_description => [],
2869 $detail->{'ref'} = $line_item->{'pkgnum'};
2870 $detail->{'quantity'} = 1;
2871 $detail->{'section'} = $previous_section;
2872 $detail->{'description'} = &$escape_function($line_item->{'description'});
2873 if ( exists $line_item->{'ext_description'} ) {
2874 @{$detail->{'ext_description'}} = map {
2875 &$escape_function($_);
2876 } @{$line_item->{'ext_description'}};
2878 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2879 $line_item->{'amount'};
2880 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2882 push @detail_items, $detail;
2883 push @buf, [ $detail->{'description'},
2884 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2890 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2891 push @buf, ['','-----------'];
2892 push @buf, [ 'Total Previous Balance',
2893 $money_char. sprintf("%10.2f", $pr_total) ];
2897 if ( $conf->exists('svc_phone-did-summary') ) {
2898 warn "$me adding DID summary\n"
2901 my ($didsummary,$minutes) = $self->_did_summary;
2902 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2904 { 'description' => $didsummary_desc,
2905 'ext_description' => [ $didsummary, $minutes ],
2909 foreach my $section (@sections, @$late_sections) {
2911 warn "$me adding section \n". Dumper($section)
2914 # begin some normalization
2915 $section->{'subtotal'} = $section->{'amount'}
2917 && !exists($section->{subtotal})
2918 && exists($section->{amount});
2920 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2921 if ( $invoice_data{finance_section} &&
2922 $section->{'description'} eq $invoice_data{finance_section} );
2924 $section->{'subtotal'} = $other_money_char.
2925 sprintf('%.2f', $section->{'subtotal'})
2928 # continue some normalization
2929 $section->{'amount'} = $section->{'subtotal'}
2933 if ( $section->{'description'} ) {
2934 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2939 warn "$me setting options\n"
2942 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2944 $options{'section'} = $section if $multisection;
2945 $options{'format'} = $format;
2946 $options{'escape_function'} = $escape_function;
2947 $options{'format_function'} = sub { () } unless $unsquelched;
2948 $options{'unsquelched'} = $unsquelched;
2949 $options{'summary_page'} = $summarypage;
2950 $options{'skip_usage'} =
2951 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2952 $options{'multilocation'} = $multilocation;
2953 $options{'multisection'} = $multisection;
2955 warn "$me searching for line items\n"
2958 foreach my $line_item ( $self->_items_pkg(%options) ) {
2960 warn "$me adding line item $line_item\n"
2964 ext_description => [],
2966 $detail->{'ref'} = $line_item->{'pkgnum'};
2967 $detail->{'quantity'} = $line_item->{'quantity'};
2968 $detail->{'section'} = $section;
2969 $detail->{'description'} = &$escape_function($line_item->{'description'});
2970 if ( exists $line_item->{'ext_description'} ) {
2971 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2973 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2974 $line_item->{'amount'};
2975 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2976 $line_item->{'unit_amount'};
2977 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2979 push @detail_items, $detail;
2980 push @buf, ( [ $detail->{'description'},
2981 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2983 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2987 if ( $section->{'description'} ) {
2988 push @buf, ( ['','-----------'],
2989 [ $section->{'description'}. ' sub-total',
2990 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2999 $invoice_data{current_less_finance} =
3000 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3002 if ( $multisection && !$conf->exists('disable_previous_balance')
3003 || $conf->exists('previous_balance-summary_only') )
3005 unshift @sections, $previous_section if $pr_total;
3008 warn "$me adding taxes\n"
3011 foreach my $tax ( $self->_items_tax ) {
3013 $taxtotal += $tax->{'amount'};
3015 my $description = &$escape_function( $tax->{'description'} );
3016 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3018 if ( $multisection ) {
3020 my $money = $old_latex ? '' : $money_char;
3021 push @detail_items, {
3022 ext_description => [],
3025 description => $description,
3026 amount => $money. $amount,
3028 section => $tax_section,
3033 push @total_items, {
3034 'total_item' => $description,
3035 'total_amount' => $other_money_char. $amount,
3040 push @buf,[ $description,
3041 $money_char. $amount,
3048 $total->{'total_item'} = 'Sub-total';
3049 $total->{'total_amount'} =
3050 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3052 if ( $multisection ) {
3053 $tax_section->{'subtotal'} = $other_money_char.
3054 sprintf('%.2f', $taxtotal);
3055 $tax_section->{'pretotal'} = 'New charges sub-total '.
3056 $total->{'total_amount'};
3057 push @sections, $tax_section if $taxtotal;
3059 unshift @total_items, $total;
3062 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3064 push @buf,['','-----------'];
3065 push @buf,[( $conf->exists('disable_previous_balance')
3067 : 'Total New Charges'
3069 $money_char. sprintf("%10.2f",$self->charged) ];
3075 $item = $conf->config('previous_balance-exclude_from_total')
3076 || 'Total New Charges'
3077 if $conf->exists('previous_balance-exclude_from_total');
3078 my $amount = $self->charged +
3079 ( $conf->exists('disable_previous_balance') ||
3080 $conf->exists('previous_balance-exclude_from_total')
3084 $total->{'total_item'} = &$embolden_function($item);
3085 $total->{'total_amount'} =
3086 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3087 if ( $multisection ) {
3088 if ( $adjust_section->{'sort_weight'} ) {
3089 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3090 sprintf("%.2f", ($self->billing_balance || 0) );
3092 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3093 sprintf('%.2f', $self->charged );
3096 push @total_items, $total;
3098 push @buf,['','-----------'];
3101 sprintf( '%10.2f', $amount )
3106 unless ( $conf->exists('disable_previous_balance') ) {
3107 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3110 my $credittotal = 0;
3111 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3114 $total->{'total_item'} = &$escape_function($credit->{'description'});
3115 $credittotal += $credit->{'amount'};
3116 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3117 $adjusttotal += $credit->{'amount'};
3118 if ( $multisection ) {
3119 my $money = $old_latex ? '' : $money_char;
3120 push @detail_items, {
3121 ext_description => [],
3124 description => &$escape_function($credit->{'description'}),
3125 amount => $money. $credit->{'amount'},
3127 section => $adjust_section,
3130 push @total_items, $total;
3134 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3137 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3138 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3142 my $paymenttotal = 0;
3143 foreach my $payment ( $self->_items_payments ) {
3145 $total->{'total_item'} = &$escape_function($payment->{'description'});
3146 $paymenttotal += $payment->{'amount'};
3147 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3148 $adjusttotal += $payment->{'amount'};
3149 if ( $multisection ) {
3150 my $money = $old_latex ? '' : $money_char;
3151 push @detail_items, {
3152 ext_description => [],
3155 description => &$escape_function($payment->{'description'}),
3156 amount => $money. $payment->{'amount'},
3158 section => $adjust_section,
3161 push @total_items, $total;
3163 push @buf, [ $payment->{'description'},
3164 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3167 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3169 if ( $multisection ) {
3170 $adjust_section->{'subtotal'} = $other_money_char.
3171 sprintf('%.2f', $adjusttotal);
3172 push @sections, $adjust_section
3173 unless $adjust_section->{sort_weight};
3178 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3179 $total->{'total_amount'} =
3180 &$embolden_function(
3181 $other_money_char. sprintf('%.2f', $summarypage
3183 $self->billing_balance
3184 : $self->owed + $pr_total
3187 if ( $multisection && !$adjust_section->{sort_weight} ) {
3188 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3189 $total->{'total_amount'};
3191 push @total_items, $total;
3193 push @buf,['','-----------'];
3194 push @buf,[$self->balance_due_msg, $money_char.
3195 sprintf("%10.2f", $balance_due ) ];
3198 if ( $conf->exists('previous_balance-show_credit')
3199 and $cust_main->balance < 0 ) {
3200 my $credit_total = {
3201 'total_item' => &$embolden_function($self->credit_balance_msg),
3202 'total_amount' => &$embolden_function(
3203 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3206 if ( $multisection ) {
3207 $adjust_section->{'posttotal'} .= $newline_token .
3208 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3211 push @total_items, $credit_total;
3213 push @buf,['','-----------'];
3214 push @buf,[$self->credit_balance_msg, $money_char.
3215 sprintf("%10.2f", -$cust_main->balance ) ];
3219 if ( $multisection ) {
3220 if ($conf->exists('svc_phone_sections')) {
3222 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3223 $total->{'total_amount'} =
3224 &$embolden_function(
3225 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3227 my $last_section = pop @sections;
3228 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3229 $total->{'total_amount'};
3230 push @sections, $last_section;
3232 push @sections, @$late_sections
3236 my @includelist = ();
3237 push @includelist, 'summary' if $summarypage;
3238 foreach my $include ( @includelist ) {
3240 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3243 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3245 @inc_src = $conf->config($inc_file, $agentnum);
3249 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3251 my $convert_map = $convert_maps{$format}{$include};
3253 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3254 s/--\@\]/$delimiters{$format}[1]/g;
3257 &$convert_map( $conf->config($inc_file, $agentnum) );
3261 my $inc_tt = new Text::Template (
3263 SOURCE => [ map "$_\n", @inc_src ],
3264 DELIMITERS => $delimiters{$format},
3265 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3267 unless ( $inc_tt->compile() ) {
3268 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3269 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3273 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3275 $invoice_data{$include} =~ s/\n+$//
3276 if ($format eq 'latex');
3281 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3282 /invoice_lines\((\d*)\)/;
3283 $invoice_lines += $1 || scalar(@buf);
3286 die "no invoice_lines() functions in template?"
3287 if ( $format eq 'template' && !$wasfunc );
3289 if ($format eq 'template') {
3291 if ( $invoice_lines ) {
3292 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3293 $invoice_data{'total_pages'}++
3294 if scalar(@buf) % $invoice_lines;
3297 #setup subroutine for the template
3298 sub FS::cust_bill::_template::invoice_lines {
3299 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3301 scalar(@FS::cust_bill::_template::buf)
3302 ? shift @FS::cust_bill::_template::buf
3311 push @collect, split("\n",
3312 $text_template->fill_in( HASH => \%invoice_data,
3313 PACKAGE => 'FS::cust_bill::_template'
3316 $FS::cust_bill::_template::page++;
3318 map "$_\n", @collect;
3320 warn "filling in template for invoice ". $self->invnum. "\n"
3322 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3325 $text_template->fill_in(HASH => \%invoice_data);
3329 # helper routine for generating date ranges
3330 sub _prior_month30s {
3333 [ 1, 2592000 ], # 0-30 days ago
3334 [ 2592000, 5184000 ], # 30-60 days ago
3335 [ 5184000, 7776000 ], # 60-90 days ago
3336 [ 7776000, 0 ], # 90+ days ago
3339 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3340 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3345 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3347 Returns an postscript invoice, as a scalar.
3349 Options can be passed as a hashref (recommended) or as a list of time, template
3350 and then any key/value pairs for any other options.
3352 I<time> an optional value used to control the printing of overdue messages. The
3353 default is now. It isn't the date of the invoice; that's the `_date' field.
3354 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3355 L<Time::Local> and L<Date::Parse> for conversion functions.
3357 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3364 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3365 my $ps = generate_ps($file);
3367 unlink($barcodefile) if $barcodefile;
3372 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3374 Returns an PDF invoice, as a scalar.
3376 Options can be passed as a hashref (recommended) or as a list of time, template
3377 and then any key/value pairs for any other options.
3379 I<time> an optional value used to control the printing of overdue messages. The
3380 default is now. It isn't the date of the invoice; that's the `_date' field.
3381 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3382 L<Time::Local> and L<Date::Parse> for conversion functions.
3384 I<template>, if specified, is the name of a suffix for alternate invoices.
3386 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3393 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3394 my $pdf = generate_pdf($file);
3396 unlink($barcodefile) if $barcodefile;
3401 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3403 Returns an HTML invoice, as a scalar.
3405 I<time> an optional value used to control the printing of overdue messages. The
3406 default is now. It isn't the date of the invoice; that's the `_date' field.
3407 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3408 L<Time::Local> and L<Date::Parse> for conversion functions.
3410 I<template>, if specified, is the name of a suffix for alternate invoices.
3412 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3414 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3415 when emailing the invoice as part of a multipart/related MIME email.
3423 %params = %{ shift() };
3425 $params{'time'} = shift;
3426 $params{'template'} = shift;
3427 $params{'cid'} = shift;
3430 $params{'format'} = 'html';
3432 $self->print_generic( %params );
3435 # quick subroutine for print_latex
3437 # There are ten characters that LaTeX treats as special characters, which
3438 # means that they do not simply typeset themselves:
3439 # # $ % & ~ _ ^ \ { }
3441 # TeX ignores blanks following an escaped character; if you want a blank (as
3442 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3446 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3447 $value =~ s/([<>])/\$$1\$/g;
3453 encode_entities($value);
3457 sub _html_escape_nbsp {
3458 my $value = _html_escape(shift);
3459 $value =~ s/ +/ /g;
3463 #utility methods for print_*
3465 sub _translate_old_latex_format {
3466 warn "_translate_old_latex_format called\n"
3473 if ( $line =~ /^%%Detail\s*$/ ) {
3475 push @template, q![@--!,
3476 q! foreach my $_tr_line (@detail_items) {!,
3477 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3478 q! $_tr_line->{'description'} .= !,
3479 q! "\\tabularnewline\n~~".!,
3480 q! join( "\\tabularnewline\n~~",!,
3481 q! @{$_tr_line->{'ext_description'}}!,
3485 while ( ( my $line_item_line = shift )
3486 !~ /^%%EndDetail\s*$/ ) {
3487 $line_item_line =~ s/'/\\'/g; # nice LTS
3488 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3489 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3490 push @template, " \$OUT .= '$line_item_line';";
3493 push @template, '}',
3496 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3498 push @template, '[@--',
3499 ' foreach my $_tr_line (@total_items) {';
3501 while ( ( my $total_item_line = shift )
3502 !~ /^%%EndTotalDetails\s*$/ ) {
3503 $total_item_line =~ s/'/\\'/g; # nice LTS
3504 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3505 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3506 push @template, " \$OUT .= '$total_item_line';";
3509 push @template, '}',
3513 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3514 push @template, $line;
3520 warn "$_\n" foreach @template;
3529 #check for an invoice-specific override
3530 return $self->invoice_terms if $self->invoice_terms;
3532 #check for a customer- specific override
3533 my $cust_main = $self->cust_main;
3534 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3536 #use configured default
3537 $conf->config('invoice_default_terms') || '';
3543 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3544 $duedate = $self->_date() + ( $1 * 86400 );
3551 $self->due_date ? time2str(shift, $self->due_date) : '';
3554 sub balance_due_msg {
3556 my $msg = 'Balance Due';
3557 return $msg unless $self->terms;
3558 if ( $self->due_date ) {
3559 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3560 } elsif ( $self->terms ) {
3561 $msg .= ' - '. $self->terms;
3566 sub balance_due_date {
3569 if ( $conf->exists('invoice_default_terms')
3570 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3571 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3576 sub credit_balance_msg { 'Credit Balance Remaining' }
3578 =item invnum_date_pretty
3580 Returns a string with the invoice number and date, for example:
3581 "Invoice #54 (3/20/2008)"
3585 sub invnum_date_pretty {
3587 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3592 Returns a string with the date, for example: "3/20/2008"
3598 time2str($date_format, $self->_date);
3601 use vars qw(%pkg_category_cache);
3602 sub _items_sections {
3605 my $summarypage = shift;
3607 my $extra_sections = shift;
3611 my %late_subtotal = ();
3614 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3617 my $usage = $cust_bill_pkg->usage;
3619 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3620 next if ( $display->summary && $summarypage );
3622 my $section = $display->section;
3623 my $type = $display->type;
3625 $not_tax{$section} = 1
3626 unless $cust_bill_pkg->pkgnum == 0;
3628 if ( $display->post_total && !$summarypage ) {
3629 if (! $type || $type eq 'S') {
3630 $late_subtotal{$section} += $cust_bill_pkg->setup
3631 if $cust_bill_pkg->setup != 0;
3635 $late_subtotal{$section} += $cust_bill_pkg->recur
3636 if $cust_bill_pkg->recur != 0;
3639 if ($type && $type eq 'R') {
3640 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3641 if $cust_bill_pkg->recur != 0;
3644 if ($type && $type eq 'U') {
3645 $late_subtotal{$section} += $usage
3646 unless scalar(@$extra_sections);
3651 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3653 if (! $type || $type eq 'S') {
3654 $subtotal{$section} += $cust_bill_pkg->setup
3655 if $cust_bill_pkg->setup != 0;
3659 $subtotal{$section} += $cust_bill_pkg->recur
3660 if $cust_bill_pkg->recur != 0;
3663 if ($type && $type eq 'R') {
3664 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3665 if $cust_bill_pkg->recur != 0;
3668 if ($type && $type eq 'U') {
3669 $subtotal{$section} += $usage
3670 unless scalar(@$extra_sections);
3679 %pkg_category_cache = ();
3681 push @$late, map { { 'description' => &{$escape}($_),
3682 'subtotal' => $late_subtotal{$_},
3684 'sort_weight' => ( _pkg_category($_)
3685 ? _pkg_category($_)->weight
3688 ((_pkg_category($_) && _pkg_category($_)->condense)
3689 ? $self->_condense_section($format)
3693 sort _sectionsort keys %late_subtotal;
3696 if ( $summarypage ) {
3697 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3698 map { $_->categoryname } qsearch('pkg_category', {});
3699 push @sections, '' if exists($subtotal{''});
3701 @sections = keys %subtotal;
3704 my @early = map { { 'description' => &{$escape}($_),
3705 'subtotal' => $subtotal{$_},
3706 'summarized' => $not_tax{$_} ? '' : 'Y',
3707 'tax_section' => $not_tax{$_} ? '' : 'Y',
3708 'sort_weight' => ( _pkg_category($_)
3709 ? _pkg_category($_)->weight
3712 ((_pkg_category($_) && _pkg_category($_)->condense)
3713 ? $self->_condense_section($format)
3718 push @early, @$extra_sections if $extra_sections;
3720 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3724 #helper subs for above
3727 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3731 my $categoryname = shift;
3732 $pkg_category_cache{$categoryname} ||=
3733 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3736 my %condensed_format = (
3737 'label' => [ qw( Description Qty Amount ) ],
3739 sub { shift->{description} },
3740 sub { shift->{quantity} },
3741 sub { my($href, %opt) = @_;
3742 ($opt{dollar} || ''). $href->{amount};
3745 'align' => [ qw( l r r ) ],
3746 'span' => [ qw( 5 1 1 ) ], # unitprices?
3747 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3750 sub _condense_section {
3751 my ( $self, $format ) = ( shift, shift );
3753 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3754 qw( description_generator
3757 total_line_generator
3762 sub _condensed_generator_defaults {
3763 my ( $self, $format ) = ( shift, shift );
3764 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3773 sub _condensed_header_generator {
3774 my ( $self, $format ) = ( shift, shift );
3776 my ( $f, $prefix, $suffix, $separator, $column ) =
3777 _condensed_generator_defaults($format);
3779 if ($format eq 'latex') {
3780 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3781 $suffix = "\\\\\n\\hline";
3784 sub { my ($d,$a,$s,$w) = @_;
3785 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3787 } elsif ( $format eq 'html' ) {
3788 $prefix = '<th></th>';
3792 sub { my ($d,$a,$s,$w) = @_;
3793 return qq!<th align="$html_align{$a}">$d</th>!;
3801 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3803 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3806 $prefix. join($separator, @result). $suffix;
3811 sub _condensed_description_generator {
3812 my ( $self, $format ) = ( shift, shift );
3814 my ( $f, $prefix, $suffix, $separator, $column ) =
3815 _condensed_generator_defaults($format);
3817 my $money_char = '$';
3818 if ($format eq 'latex') {
3819 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3821 $separator = " & \n";
3823 sub { my ($d,$a,$s,$w) = @_;
3824 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3826 $money_char = '\\dollar';
3827 }elsif ( $format eq 'html' ) {
3828 $prefix = '"><td align="center"></td>';
3832 sub { my ($d,$a,$s,$w) = @_;
3833 return qq!<td align="$html_align{$a}">$d</td>!;
3835 #$money_char = $conf->config('money_char') || '$';
3836 $money_char = ''; # this is madness
3844 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3846 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3848 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3849 map { $f->{$_}->[$i] } qw(align span width)
3853 $prefix. join( $separator, @result ). $suffix;
3858 sub _condensed_total_generator {
3859 my ( $self, $format ) = ( shift, shift );
3861 my ( $f, $prefix, $suffix, $separator, $column ) =
3862 _condensed_generator_defaults($format);
3865 if ($format eq 'latex') {
3868 $separator = " & \n";
3870 sub { my ($d,$a,$s,$w) = @_;
3871 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3873 }elsif ( $format eq 'html' ) {
3877 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3879 sub { my ($d,$a,$s,$w) = @_;
3880 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3889 # my $r = &{$f->{fields}->[$i]}(@args);
3890 # $r .= ' Total' unless $i;
3892 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3894 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3895 map { $f->{$_}->[$i] } qw(align span width)
3899 $prefix. join( $separator, @result ). $suffix;
3904 =item total_line_generator FORMAT
3906 Returns a coderef used for generation of invoice total line items for this
3907 usage_class. FORMAT is either html or latex
3911 # should not be used: will have issues with hash element names (description vs
3912 # total_item and amount vs total_amount -- another array of functions?
3914 sub _condensed_total_line_generator {
3915 my ( $self, $format ) = ( shift, shift );
3917 my ( $f, $prefix, $suffix, $separator, $column ) =
3918 _condensed_generator_defaults($format);
3921 if ($format eq 'latex') {
3924 $separator = " & \n";
3926 sub { my ($d,$a,$s,$w) = @_;
3927 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3929 }elsif ( $format eq 'html' ) {
3933 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3935 sub { my ($d,$a,$s,$w) = @_;
3936 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3945 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3947 &{$column}( &{$f->{fields}->[$i]}(@args),
3948 map { $f->{$_}->[$i] } qw(align span width)
3952 $prefix. join( $separator, @result ). $suffix;
3957 #sub _items_extra_usage_sections {
3959 # my $escape = shift;
3961 # my %sections = ();
3963 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3964 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3966 # next unless $cust_bill_pkg->pkgnum > 0;
3968 # foreach my $section ( keys %usage_class ) {
3970 # my $usage = $cust_bill_pkg->usage($section);
3972 # next unless $usage && $usage > 0;
3974 # $sections{$section} ||= 0;
3975 # $sections{$section} += $usage;
3981 # map { { 'description' => &{$escape}($_),
3982 # 'subtotal' => $sections{$_},
3983 # 'summarized' => '',
3984 # 'tax_section' => '',
3987 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3991 sub _items_extra_usage_sections {
4000 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4001 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4002 next unless $cust_bill_pkg->pkgnum > 0;
4004 foreach my $classnum ( keys %usage_class ) {
4005 my $section = $usage_class{$classnum}->classname;
4006 $classnums{$section} = $classnum;
4008 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4009 my $amount = $detail->amount;
4010 next unless $amount && $amount > 0;
4012 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4013 $sections{$section}{amount} += $amount; #subtotal
4014 $sections{$section}{calls}++;
4015 $sections{$section}{duration} += $detail->duration;
4017 my $desc = $detail->regionname;
4018 my $description = $desc;
4019 $description = substr($desc, 0, 50). '...'
4020 if $format eq 'latex' && length($desc) > 50;
4022 $lines{$section}{$desc} ||= {
4023 description => &{$escape}($description),
4024 #pkgpart => $part_pkg->pkgpart,
4025 pkgnum => $cust_bill_pkg->pkgnum,
4030 #unit_amount => $cust_bill_pkg->unitrecur,
4031 quantity => $cust_bill_pkg->quantity,
4032 product_code => 'N/A',
4033 ext_description => [],
4036 $lines{$section}{$desc}{amount} += $amount;
4037 $lines{$section}{$desc}{calls}++;
4038 $lines{$section}{$desc}{duration} += $detail->duration;
4044 my %sectionmap = ();
4045 foreach (keys %sections) {
4046 my $usage_class = $usage_class{$classnums{$_}};
4047 $sectionmap{$_} = { 'description' => &{$escape}($_),
4048 'amount' => $sections{$_}{amount}, #subtotal
4049 'calls' => $sections{$_}{calls},
4050 'duration' => $sections{$_}{duration},
4052 'tax_section' => '',
4053 'sort_weight' => $usage_class->weight,
4054 ( $usage_class->format
4055 ? ( map { $_ => $usage_class->$_($format) }
4056 qw( description_generator header_generator total_generator total_line_generator )
4063 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4067 foreach my $section ( keys %lines ) {
4068 foreach my $line ( keys %{$lines{$section}} ) {
4069 my $l = $lines{$section}{$line};
4070 $l->{section} = $sectionmap{$section};
4071 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4072 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4077 return(\@sections, \@lines);
4083 my $end = $self->_date;
4085 # start at date of previous invoice + 1 second or 0 if no previous invoice
4086 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4087 $start = 0 if !$start;
4090 my $cust_main = $self->cust_main;
4091 my @pkgs = $cust_main->all_pkgs;
4092 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4095 foreach my $pkg ( @pkgs ) {
4096 my @h_cust_svc = $pkg->h_cust_svc($end);
4097 foreach my $h_cust_svc ( @h_cust_svc ) {
4098 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4099 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4101 my $inserted = $h_cust_svc->date_inserted;
4102 my $deleted = $h_cust_svc->date_deleted;
4103 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4105 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4107 # DID either activated or ported in; cannot be both for same DID simultaneously
4108 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4109 && (!$phone_inserted->lnp_status
4110 || $phone_inserted->lnp_status eq ''
4111 || $phone_inserted->lnp_status eq 'native')) {
4114 else { # this one not so clean, should probably move to (h_)svc_phone
4115 my $phone_portedin = qsearchs( 'h_svc_phone',
4116 { 'svcnum' => $h_cust_svc->svcnum,
4117 'lnp_status' => 'portedin' },
4118 FS::h_svc_phone->sql_h_searchs($end),
4120 $num_portedin++ if $phone_portedin;
4123 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4124 if($deleted >= $start && $deleted <= $end && $phone_deleted
4125 && (!$phone_deleted->lnp_status
4126 || $phone_deleted->lnp_status ne 'portingout')) {
4129 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4130 && $phone_deleted->lnp_status
4131 && $phone_deleted->lnp_status eq 'portingout') {
4135 # increment usage minutes
4136 if ( $phone_inserted ) {
4137 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4138 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4141 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4144 # don't look at this service again
4145 push @seen, $h_cust_svc->svcnum;
4149 $minutes = sprintf("%d", $minutes);
4150 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4151 . "$num_deactivated Ported-Out: $num_portedout ",
4152 "Total Minutes: $minutes");
4155 sub _items_accountcode_cdr {
4160 my $section = { 'amount' => 0,
4163 'sort_weight' => '',
4165 'description' => 'Usage by Account Code',
4171 my %accountcodes = ();
4173 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4174 next unless $cust_bill_pkg->pkgnum > 0;
4176 my @header = $cust_bill_pkg->details_header;
4177 next unless scalar(@header);
4178 $section->{'header'} = join(',',@header);
4180 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4182 $section->{'header'} = $detail->formatted('format' => $format)
4183 if($detail->detail eq $section->{'header'});
4185 my $accountcode = $detail->accountcode;
4186 next unless $accountcode;
4188 my $amount = $detail->amount;
4189 next unless $amount && $amount > 0;
4191 $accountcodes{$accountcode} ||= {
4192 description => $accountcode,
4199 product_code => 'N/A',
4200 section => $section,
4201 ext_description => [],
4204 $section->{'amount'} += $amount;
4205 $accountcodes{$accountcode}{'amount'} += $amount;
4206 $accountcodes{$accountcode}{calls}++;
4207 $accountcodes{$accountcode}{duration} += $detail->duration;
4208 push @{$accountcodes{$accountcode}{ext_description}},
4209 $detail->formatted('format' => $format);
4213 foreach my $l ( values %accountcodes ) {
4214 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4215 unshift @{$l->{ext_description}}, $section->{'header'};
4219 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4221 return ($section,\@sorted_lines);
4224 sub _items_svc_phone_sections {
4233 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4234 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4236 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4237 next unless $cust_bill_pkg->pkgnum > 0;
4239 my @header = $cust_bill_pkg->details_header;
4240 next unless scalar(@header);
4242 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4244 my $phonenum = $detail->phonenum;
4245 next unless $phonenum;
4247 my $amount = $detail->amount;
4248 next unless $amount && $amount > 0;
4250 $sections{$phonenum} ||= { 'amount' => 0,
4253 'sort_weight' => -1,
4254 'phonenum' => $phonenum,
4256 $sections{$phonenum}{amount} += $amount; #subtotal
4257 $sections{$phonenum}{calls}++;
4258 $sections{$phonenum}{duration} += $detail->duration;
4260 my $desc = $detail->regionname;
4261 my $description = $desc;
4262 $description = substr($desc, 0, 50). '...'
4263 if $format eq 'latex' && length($desc) > 50;
4265 $lines{$phonenum}{$desc} ||= {
4266 description => &{$escape}($description),
4267 #pkgpart => $part_pkg->pkgpart,
4275 product_code => 'N/A',
4276 ext_description => [],
4279 $lines{$phonenum}{$desc}{amount} += $amount;
4280 $lines{$phonenum}{$desc}{calls}++;
4281 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4283 my $line = $usage_class{$detail->classnum}->classname;
4284 $sections{"$phonenum $line"} ||=
4288 'sort_weight' => $usage_class{$detail->classnum}->weight,
4289 'phonenum' => $phonenum,
4290 'header' => [ @header ],
4292 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4293 $sections{"$phonenum $line"}{calls}++;
4294 $sections{"$phonenum $line"}{duration} += $detail->duration;
4296 $lines{"$phonenum $line"}{$desc} ||= {
4297 description => &{$escape}($description),
4298 #pkgpart => $part_pkg->pkgpart,
4306 product_code => 'N/A',
4307 ext_description => [],
4310 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4311 $lines{"$phonenum $line"}{$desc}{calls}++;
4312 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4313 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4314 $detail->formatted('format' => $format);
4319 my %sectionmap = ();
4320 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4321 foreach ( keys %sections ) {
4322 my @header = @{ $sections{$_}{header} || [] };
4324 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4325 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4326 my $usage_class = $summary ? $simple : $usage_simple;
4327 my $ending = $summary ? ' usage charges' : '';
4330 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4332 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4333 'amount' => $sections{$_}{amount}, #subtotal
4334 'calls' => $sections{$_}{calls},
4335 'duration' => $sections{$_}{duration},
4337 'tax_section' => '',
4338 'phonenum' => $sections{$_}{phonenum},
4339 'sort_weight' => $sections{$_}{sort_weight},
4340 'post_total' => $summary, #inspire pagebreak
4342 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4343 qw( description_generator
4346 total_line_generator
4353 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4354 $a->{sort_weight} <=> $b->{sort_weight}
4359 foreach my $section ( keys %lines ) {
4360 foreach my $line ( keys %{$lines{$section}} ) {
4361 my $l = $lines{$section}{$line};
4362 $l->{section} = $sectionmap{$section};
4363 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4364 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4369 if($conf->exists('phone_usage_class_summary')) {
4370 # this only works with Latex
4374 # after this, we'll have only two sections per DID:
4375 # Calls Summary and Calls Detail
4376 foreach my $section ( @sections ) {
4377 if($section->{'post_total'}) {
4378 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4379 $section->{'total_line_generator'} = sub { '' };
4380 $section->{'total_generator'} = sub { '' };
4381 $section->{'header_generator'} = sub { '' };
4382 $section->{'description_generator'} = '';
4383 push @newsections, $section;
4384 my %calls_detail = %$section;
4385 $calls_detail{'post_total'} = '';
4386 $calls_detail{'sort_weight'} = '';
4387 $calls_detail{'description_generator'} = sub { '' };
4388 $calls_detail{'header_generator'} = sub {
4389 return ' & Date/Time & Called Number & Duration & Price'
4390 if $format eq 'latex';
4393 $calls_detail{'description'} = 'Calls Detail: '
4394 . $section->{'phonenum'};
4395 push @newsections, \%calls_detail;
4399 # after this, each usage class is collapsed/summarized into a single
4400 # line under the Calls Summary section
4401 foreach my $newsection ( @newsections ) {
4402 if($newsection->{'post_total'}) { # this means Calls Summary
4403 foreach my $section ( @sections ) {
4404 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4405 && !$section->{'post_total'});
4406 my $newdesc = $section->{'description'};
4407 my $tn = $section->{'phonenum'};
4408 $newdesc =~ s/$tn//g;
4409 my $line = { ext_description => [],
4413 calls => $section->{'calls'},
4414 section => $newsection,
4415 duration => $section->{'duration'},
4416 description => $newdesc,
4417 amount => sprintf("%.2f",$section->{'amount'}),
4418 product_code => 'N/A',
4420 push @newlines, $line;
4425 # after this, Calls Details is populated with all CDRs
4426 foreach my $newsection ( @newsections ) {
4427 if(!$newsection->{'post_total'}) { # this means Calls Details
4428 foreach my $line ( @lines ) {
4429 next unless (scalar(@{$line->{'ext_description'}}) &&
4430 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4432 my @extdesc = @{$line->{'ext_description'}};
4434 foreach my $extdesc ( @extdesc ) {
4435 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4436 push @newextdesc, $extdesc;
4438 $line->{'ext_description'} = \@newextdesc;
4439 $line->{'section'} = $newsection;
4440 push @newlines, $line;
4445 return(\@newsections, \@newlines);
4448 return(\@sections, \@lines);
4455 #my @display = scalar(@_)
4457 # : qw( _items_previous _items_pkg );
4458 # #: qw( _items_pkg );
4459 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4460 my @display = qw( _items_previous _items_pkg );
4463 foreach my $display ( @display ) {
4464 push @b, $self->$display(@_);
4469 sub _items_previous {
4471 my $cust_main = $self->cust_main;
4472 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4474 foreach ( @pr_cust_bill ) {
4475 my $date = $conf->exists('invoice_show_prior_due_date')
4476 ? 'due '. $_->due_date2str($date_format)
4477 : time2str($date_format, $_->_date);
4479 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4480 #'pkgpart' => 'N/A',
4482 'amount' => sprintf("%.2f", $_->owed),
4488 # 'description' => 'Previous Balance',
4489 # #'pkgpart' => 'N/A',
4490 # 'pkgnum' => 'N/A',
4491 # 'amount' => sprintf("%10.2f", $pr_total ),
4492 # 'ext_description' => [ map {
4493 # "Invoice ". $_->invnum.
4494 # " (". time2str("%x",$_->_date). ") ".
4495 # sprintf("%10.2f", $_->owed)
4496 # } @pr_cust_bill ],
4505 warn "$me _items_pkg searching for all package line items\n"
4508 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4510 warn "$me _items_pkg filtering line items\n"
4512 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4514 if ($options{section} && $options{section}->{condensed}) {
4516 warn "$me _items_pkg condensing section\n"
4520 local $Storable::canonical = 1;
4521 foreach ( @items ) {
4523 delete $item->{ref};
4524 delete $item->{ext_description};
4525 my $key = freeze($item);
4526 $itemshash{$key} ||= 0;
4527 $itemshash{$key} ++; # += $item->{quantity};
4529 @items = sort { $a->{description} cmp $b->{description} }
4530 map { my $i = thaw($_);
4531 $i->{quantity} = $itemshash{$_};
4533 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4539 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4546 return 0 unless $a->itemdesc cmp $b->itemdesc;
4547 return -1 if $b->itemdesc eq 'Tax';
4548 return 1 if $a->itemdesc eq 'Tax';
4549 return -1 if $b->itemdesc eq 'Other surcharges';
4550 return 1 if $a->itemdesc eq 'Other surcharges';
4551 $a->itemdesc cmp $b->itemdesc;
4556 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4557 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4560 sub _items_cust_bill_pkg {
4562 my $cust_bill_pkgs = shift;
4565 my $format = $opt{format} || '';
4566 my $escape_function = $opt{escape_function} || sub { shift };
4567 my $format_function = $opt{format_function} || '';
4568 my $unsquelched = $opt{unsquelched} || '';
4569 my $section = $opt{section}->{description} if $opt{section};
4570 my $summary_page = $opt{summary_page} || '';
4571 my $multilocation = $opt{multilocation} || '';
4572 my $multisection = $opt{multisection} || '';
4573 my $discount_show_always = 0;
4576 my ($s, $r, $u) = ( undef, undef, undef );
4577 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4580 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4581 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4584 foreach my $display ( grep { defined($section)
4585 ? $_->section eq $section
4588 #grep { !$_->summary || !$summary_page } # bunk!
4589 grep { !$_->summary || $multisection }
4590 $cust_bill_pkg->cust_bill_pkg_display
4594 warn "$me _items_cust_bill_pkg considering display item $display\n"
4597 my $type = $display->type;
4599 my $desc = $cust_bill_pkg->desc;
4600 $desc = substr($desc, 0, 50). '...'
4601 if $format eq 'latex' && length($desc) > 50;
4603 my %details_opt = ( 'format' => $format,
4604 'escape_function' => $escape_function,
4605 'format_function' => $format_function,
4608 if ( $cust_bill_pkg->pkgnum > 0 ) {
4610 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4613 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4615 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4617 warn "$me _items_cust_bill_pkg adding setup\n"
4620 my $description = $desc;
4621 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4624 unless ( $cust_pkg->part_pkg->hide_svc_detail
4625 || $cust_bill_pkg->hidden )
4628 push @d, map &{$escape_function}($_),
4629 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4630 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4632 if ( $multilocation ) {
4633 my $loc = $cust_pkg->location_label;
4634 $loc = substr($loc, 0, 50). '...'
4635 if $format eq 'latex' && length($loc) > 50;
4636 push @d, &{$escape_function}($loc);
4641 push @d, $cust_bill_pkg->details(%details_opt)
4642 if $cust_bill_pkg->recur == 0;
4644 if ( $cust_bill_pkg->hidden ) {
4645 $s->{amount} += $cust_bill_pkg->setup;
4646 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4647 push @{ $s->{ext_description} }, @d;
4650 description => $description,
4651 #pkgpart => $part_pkg->pkgpart,
4652 pkgnum => $cust_bill_pkg->pkgnum,
4653 amount => $cust_bill_pkg->setup,
4654 unit_amount => $cust_bill_pkg->unitsetup,
4655 quantity => $cust_bill_pkg->quantity,
4656 ext_description => \@d,
4662 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4664 $cust_bill_pkg->recur != 0
4665 || $cust_bill_pkg->setup == 0
4666 || $discount_show_always
4667 || $cust_bill_pkg->recur_show_zero
4672 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4675 my $is_summary = $display->summary;
4676 my $description = ($is_summary && $type && $type eq 'U')
4677 ? "Usage charges" : $desc;
4679 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4680 " - ". time2str($date_format, $cust_bill_pkg->edate).
4682 unless $conf->exists('disable_line_item_date_ranges')
4683 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4687 #at least until cust_bill_pkg has "past" ranges in addition to
4688 #the "future" sdate/edate ones... see #3032
4689 my @dates = ( $self->_date );
4690 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4691 push @dates, $prev->sdate if $prev;
4692 push @dates, undef if !$prev;
4694 unless ( $cust_pkg->part_pkg->hide_svc_detail
4695 || $cust_bill_pkg->itemdesc
4696 || $cust_bill_pkg->hidden
4697 || $is_summary && $type && $type eq 'U' )
4700 warn "$me _items_cust_bill_pkg adding service details\n"
4703 push @d, map &{$escape_function}($_),
4704 $cust_pkg->h_labels_short(@dates, 'I')
4705 #$cust_bill_pkg->edate,
4706 #$cust_bill_pkg->sdate)
4707 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4709 warn "$me _items_cust_bill_pkg done adding service details\n"
4712 if ( $multilocation ) {
4713 my $loc = $cust_pkg->location_label;
4714 $loc = substr($loc, 0, 50). '...'
4715 if $format eq 'latex' && length($loc) > 50;
4716 push @d, &{$escape_function}($loc);
4721 unless ( $is_summary ) {
4722 warn "$me _items_cust_bill_pkg adding details\n"
4725 #instead of omitting details entirely in this case (unwanted side
4726 # effects), just omit CDRs
4727 $details_opt{'format_function'} = sub { () }
4728 if $type && $type eq 'R';
4730 push @d, $cust_bill_pkg->details(%details_opt);
4733 warn "$me _items_cust_bill_pkg calculating amount\n"
4738 $amount = $cust_bill_pkg->recur;
4739 } elsif ($type eq 'R') {
4740 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4741 } elsif ($type eq 'U') {
4742 $amount = $cust_bill_pkg->usage;
4745 if ( !$type || $type eq 'R' ) {
4747 warn "$me _items_cust_bill_pkg adding recur\n"
4750 if ( $cust_bill_pkg->hidden ) {
4751 $r->{amount} += $amount;
4752 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4753 push @{ $r->{ext_description} }, @d;
4756 description => $description,
4757 #pkgpart => $part_pkg->pkgpart,
4758 pkgnum => $cust_bill_pkg->pkgnum,
4760 unit_amount => $cust_bill_pkg->unitrecur,
4761 quantity => $cust_bill_pkg->quantity,
4762 ext_description => \@d,
4766 } else { # $type eq 'U'
4768 warn "$me _items_cust_bill_pkg adding usage\n"
4771 if ( $cust_bill_pkg->hidden ) {
4772 $u->{amount} += $amount;
4773 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4774 push @{ $u->{ext_description} }, @d;
4777 description => $description,
4778 #pkgpart => $part_pkg->pkgpart,
4779 pkgnum => $cust_bill_pkg->pkgnum,
4781 unit_amount => $cust_bill_pkg->unitrecur,
4782 quantity => $cust_bill_pkg->quantity,
4783 ext_description => \@d,
4788 } # recurring or usage with recurring charge
4790 } else { #pkgnum tax or one-shot line item (??)
4792 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4795 if ( $cust_bill_pkg->setup != 0 ) {
4797 'description' => $desc,
4798 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4801 if ( $cust_bill_pkg->recur != 0 ) {
4803 'description' => "$desc (".
4804 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4805 time2str($date_format, $cust_bill_pkg->edate). ')',
4806 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4814 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4815 && $conf->exists('discount-show-always'));
4817 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4818 if ( $_ && !$cust_bill_pkg->hidden ) {
4819 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4820 $_->{amount} =~ s/^\-0\.00$/0.00/;
4821 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4823 if $_->{amount} != 0
4824 || $discount_show_always
4825 || $cust_bill_pkg->recur_show_zero;
4832 #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4834 # $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4835 # $_->{amount} =~ s/^\-0\.00$/0.00/;
4836 # $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4838 # if $_->{amount} != 0
4839 # || $discount_show_always
4843 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4850 sub _items_credits {
4851 my( $self, %opt ) = @_;
4852 my $trim_len = $opt{'trim_len'} || 60;
4856 foreach ( $self->cust_credited ) {
4858 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4860 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4861 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4862 $reason = " ($reason) " if $reason;
4865 #'description' => 'Credit ref\#'. $_->crednum.
4866 # " (". time2str("%x",$_->cust_credit->_date) .")".
4868 'description' => 'Credit applied '.
4869 time2str($date_format,$_->cust_credit->_date). $reason,
4870 'amount' => sprintf("%.2f",$_->amount),
4878 sub _items_payments {
4882 #get & print payments
4883 foreach ( $self->cust_bill_pay ) {
4885 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4888 'description' => "Payment received ".
4889 time2str($date_format,$_->cust_pay->_date ),
4890 'amount' => sprintf("%.2f", $_->amount )
4898 =item call_details [ OPTION => VALUE ... ]
4900 Returns an array of CSV strings representing the call details for this invoice
4901 The only option available is the boolean prepend_billed_number
4906 my ($self, %opt) = @_;
4908 my $format_function = sub { shift };
4910 if ($opt{prepend_billed_number}) {
4911 $format_function = sub {
4915 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4920 my @details = map { $_->details( 'format_function' => $format_function,
4921 'escape_function' => sub{ return() },
4925 $self->cust_bill_pkg;
4926 my $header = $details[0];
4927 ( $header, grep { $_ ne $header } @details );
4937 =item process_reprint
4941 sub process_reprint {
4942 process_re_X('print', @_);
4945 =item process_reemail
4949 sub process_reemail {
4950 process_re_X('email', @_);
4958 process_re_X('fax', @_);
4966 process_re_X('ftp', @_);
4973 sub process_respool {
4974 process_re_X('spool', @_);
4977 use Storable qw(thaw);
4981 my( $method, $job ) = ( shift, shift );
4982 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4984 my $param = thaw(decode_base64(shift));
4985 warn Dumper($param) if $DEBUG;
4996 my($method, $job, %param ) = @_;
4998 warn "re_X $method for job $job with param:\n".
4999 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5002 #some false laziness w/search/cust_bill.html
5004 my $orderby = 'ORDER BY cust_bill._date';
5006 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5008 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5010 my @cust_bill = qsearch( {
5011 #'select' => "cust_bill.*",
5012 'table' => 'cust_bill',
5013 'addl_from' => $addl_from,
5015 'extra_sql' => $extra_sql,
5016 'order_by' => $orderby,
5020 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5022 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5025 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5026 foreach my $cust_bill ( @cust_bill ) {
5027 $cust_bill->$method();
5029 if ( $job ) { #progressbar foo
5031 if ( time - $min_sec > $last ) {
5032 my $error = $job->update_statustext(
5033 int( 100 * $num / scalar(@cust_bill) )
5035 die $error if $error;
5046 =head1 CLASS METHODS
5052 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5057 my ($class, $start, $end) = @_;
5059 $class->paid_sql($start, $end). ' - '.
5060 $class->credited_sql($start, $end);
5065 Returns an SQL fragment to retreive the net amount (charged minus credited).
5070 my ($class, $start, $end) = @_;
5071 'charged - '. $class->credited_sql($start, $end);
5076 Returns an SQL fragment to retreive the amount paid against this invoice.
5081 my ($class, $start, $end) = @_;
5082 $start &&= "AND cust_bill_pay._date <= $start";
5083 $end &&= "AND cust_bill_pay._date > $end";
5084 $start = '' unless defined($start);
5085 $end = '' unless defined($end);
5086 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5087 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5092 Returns an SQL fragment to retreive the amount credited against this invoice.
5097 my ($class, $start, $end) = @_;
5098 $start &&= "AND cust_credit_bill._date <= $start";
5099 $end &&= "AND cust_credit_bill._date > $end";
5100 $start = '' unless defined($start);
5101 $end = '' unless defined($end);
5102 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5103 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5108 Returns an SQL fragment to retrieve the due date of an invoice.
5109 Currently only supported on PostgreSQL.
5117 cust_bill.invoice_terms,
5118 cust_main.invoice_terms,
5119 \''.($conf->config('invoice_default_terms') || '').'\'
5120 ), E\'Net (\\\\d+)\'
5122 ) * 86400 + cust_bill._date'
5125 =item search_sql_where HASHREF
5127 Class method which returns an SQL WHERE fragment to search for parameters
5128 specified in HASHREF. Valid parameters are
5134 List reference of start date, end date, as UNIX timestamps.
5144 List reference of charged limits (exclusive).
5148 List reference of charged limits (exclusive).
5152 flag, return open invoices only
5156 flag, return net invoices only
5160 =item newest_percust
5164 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5168 sub search_sql_where {
5169 my($class, $param) = @_;
5171 warn "$me search_sql_where called with params: \n".
5172 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5178 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5179 push @search, "cust_main.agentnum = $1";
5183 if ( $param->{_date} ) {
5184 my($beginning, $ending) = @{$param->{_date}};
5186 push @search, "cust_bill._date >= $beginning",
5187 "cust_bill._date < $ending";
5191 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5192 push @search, "cust_bill.invnum >= $1";
5194 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5195 push @search, "cust_bill.invnum <= $1";
5199 if ( $param->{charged} ) {
5200 my @charged = ref($param->{charged})
5201 ? @{ $param->{charged} }
5202 : ($param->{charged});
5204 push @search, map { s/^charged/cust_bill.charged/; $_; }
5208 my $owed_sql = FS::cust_bill->owed_sql;
5211 if ( $param->{owed} ) {
5212 my @owed = ref($param->{owed})
5213 ? @{ $param->{owed} }
5215 push @search, map { s/^owed/$owed_sql/; $_; }
5220 push @search, "0 != $owed_sql"
5221 if $param->{'open'};
5222 push @search, '0 != '. FS::cust_bill->net_sql
5226 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5227 if $param->{'days'};
5230 if ( $param->{'newest_percust'} ) {
5232 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5233 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5235 my @newest_where = map { my $x = $_;
5236 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5239 grep ! /^cust_main./, @search;
5240 my $newest_where = scalar(@newest_where)
5241 ? ' AND '. join(' AND ', @newest_where)
5245 push @search, "cust_bill._date = (
5246 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5247 WHERE newest_cust_bill.custnum = cust_bill.custnum
5253 #agent virtualization
5254 my $curuser = $FS::CurrentUser::CurrentUser;
5255 if ( $curuser->username eq 'fs_queue'
5256 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5258 my $newuser = qsearchs('access_user', {
5259 'username' => $username,
5263 $curuser = $newuser;
5265 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5268 push @search, $curuser->agentnums_sql;
5270 join(' AND ', @search );
5282 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5283 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base