4 use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use Storable qw( freeze thaw );
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main_Mixin;
20 use FS::cust_statement;
21 use FS::cust_bill_pkg;
22 use FS::cust_bill_pkg_display;
23 use FS::cust_bill_pkg_detail;
27 use FS::cust_credit_bill;
29 use FS::cust_pay_batch;
30 use FS::cust_bill_event;
33 use FS::cust_bill_pay;
34 use FS::cust_bill_pay_batch;
35 use FS::part_bill_event;
38 @ISA = qw( FS::cust_main_Mixin FS::Record );
41 $me = '[FS::cust_bill]';
43 #ask FS::UID to run this stuff for us later
44 FS::UID->install_callback( sub {
46 $money_char = $conf->config('money_char') || '$';
47 $date_format = $conf->config('date_format') || '%x';
48 $rdate_format = $conf->config('date_format') || '%m/%d/%Y';
53 FS::cust_bill - Object methods for cust_bill records
59 $record = new FS::cust_bill \%hash;
60 $record = new FS::cust_bill { 'column' => 'value' };
62 $error = $record->insert;
64 $error = $new_record->replace($old_record);
66 $error = $record->delete;
68 $error = $record->check;
70 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
72 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
74 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
76 @cust_pay_objects = $cust_bill->cust_pay;
78 $tax_amount = $record->tax;
80 @lines = $cust_bill->print_text;
81 @lines = $cust_bill->print_text $time;
85 An FS::cust_bill object represents an invoice; a declaration that a customer
86 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
87 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
88 following fields are currently supported:
94 =item invnum - primary key (assigned automatically for new invoices)
96 =item custnum - customer (see L<FS::cust_main>)
98 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
99 L<Time::Local> and L<Date::Parse> for conversion functions.
101 =item charged - amount of this invoice
103 =item invoice_terms - optional terms override for this specific invoice
107 Customer info at invoice generation time
111 =item previous_balance
113 =item billing_balance
121 =item printed - deprecated
129 =item closed - books closed flag, empty or `Y'
131 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
133 =item agent_invid - legacy invoice number
143 Creates a new invoice. To add the invoice to the database, see L<"insert">.
144 Invoices are normally created by calling the bill method of a customer object
145 (see L<FS::cust_main>).
149 sub table { 'cust_bill'; }
151 sub cust_linked { $_[0]->cust_main_custnum; }
152 sub cust_unlinked_msg {
154 "WARNING: can't find cust_main.custnum ". $self->custnum.
155 ' (cust_bill.invnum '. $self->invnum. ')';
160 Adds this invoice to the database ("Posts" the invoice). If there is an error,
161 returns the error, otherwise returns false.
165 This method now works but you probably shouldn't use it. Instead, apply a
166 credit against the invoice.
168 Using this method to delete invoices outright is really, really bad. There
169 would be no record you ever posted this invoice, and there are no check to
170 make sure charged = 0 or that there are no associated cust_bill_pkg records.
172 Really, don't use it.
178 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
180 local $SIG{HUP} = 'IGNORE';
181 local $SIG{INT} = 'IGNORE';
182 local $SIG{QUIT} = 'IGNORE';
183 local $SIG{TERM} = 'IGNORE';
184 local $SIG{TSTP} = 'IGNORE';
185 local $SIG{PIPE} = 'IGNORE';
187 my $oldAutoCommit = $FS::UID::AutoCommit;
188 local $FS::UID::AutoCommit = 0;
191 foreach my $table (qw(
203 foreach my $linked ( $self->$table() ) {
204 my $error = $linked->delete;
206 $dbh->rollback if $oldAutoCommit;
213 my $error = $self->SUPER::delete(@_);
215 $dbh->rollback if $oldAutoCommit;
219 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
225 =item replace OLD_RECORD
227 Replaces the OLD_RECORD with this one in the database. If there is an error,
228 returns the error, otherwise returns false.
230 Only printed may be changed. printed is normally updated by calling the
231 collect method of a customer object (see L<FS::cust_main>).
235 #replace can be inherited from Record.pm
237 # replace_check is now the preferred way to #implement replace data checks
238 # (so $object->replace() works without an argument)
241 my( $new, $old ) = ( shift, shift );
242 return "Can't change custnum!" unless $old->custnum == $new->custnum;
243 #return "Can't change _date!" unless $old->_date eq $new->_date;
244 return "Can't change _date!" unless $old->_date == $new->_date;
245 return "Can't change charged!" unless $old->charged == $new->charged
246 || $old->charged == 0;
253 Checks all fields to make sure this is a valid invoice. If there is an error,
254 returns the error, otherwise returns false. Called by the insert and replace
263 $self->ut_numbern('invnum')
264 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
265 || $self->ut_numbern('_date')
266 || $self->ut_money('charged')
267 || $self->ut_numbern('printed')
268 || $self->ut_enum('closed', [ '', 'Y' ])
269 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
270 || $self->ut_numbern('agent_invid') #varchar?
272 return $error if $error;
274 $self->_date(time) unless $self->_date;
276 $self->printed(0) if $self->printed eq '';
283 Returns the displayed invoice number for this invoice: agent_invid if
284 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
290 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
291 return $self->agent_invid;
293 return $self->invnum;
299 Returns a list consisting of the total previous balance for this customer,
300 followed by the previous outstanding invoices (as FS::cust_bill objects also).
307 my @cust_bill = sort { $a->_date <=> $b->_date }
308 grep { $_->owed != 0 && $_->_date < $self->_date }
309 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
311 foreach ( @cust_bill ) { $total += $_->owed; }
317 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
324 { 'table' => 'cust_bill_pkg',
325 'hashref' => { 'invnum' => $self->invnum },
326 'order_by' => 'ORDER BY billpkgnum',
331 =item cust_bill_pkg_pkgnum PKGNUM
333 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
338 sub cust_bill_pkg_pkgnum {
339 my( $self, $pkgnum ) = @_;
341 { 'table' => 'cust_bill_pkg',
342 'hashref' => { 'invnum' => $self->invnum,
345 'order_by' => 'ORDER BY billpkgnum',
352 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
359 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
360 $self->cust_bill_pkg;
362 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
367 Returns true if any of the packages (or their definitions) corresponding to the
368 line items for this invoice have the no_auto flag set.
374 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
377 =item open_cust_bill_pkg
379 Returns the open line items for this invoice.
381 Note that cust_bill_pkg with both setup and recur fees are returned as two
382 separate line items, each with only one fee.
386 # modeled after cust_main::open_cust_bill
387 sub open_cust_bill_pkg {
390 # grep { $_->owed > 0 } $self->cust_bill_pkg
392 my %other = ( 'recur' => 'setup',
393 'setup' => 'recur', );
395 foreach my $field ( qw( recur setup )) {
396 push @open, map { $_->set( $other{$field}, 0 ); $_; }
397 grep { $_->owed($field) > 0 }
398 $self->cust_bill_pkg;
404 =item cust_bill_event
406 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
410 sub cust_bill_event {
412 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
415 =item num_cust_bill_event
417 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
421 sub num_cust_bill_event {
424 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
425 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
426 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
427 $sth->fetchrow_arrayref->[0];
432 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
436 #false laziness w/cust_pkg.pm
440 'table' => 'cust_event',
441 'addl_from' => 'JOIN part_event USING ( eventpart )',
442 'hashref' => { 'tablenum' => $self->invnum },
443 'extra_sql' => " AND eventtable = 'cust_bill' ",
449 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
453 #false laziness w/cust_pkg.pm
457 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
458 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
459 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
460 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
461 $sth->fetchrow_arrayref->[0];
466 Returns the customer (see L<FS::cust_main>) for this invoice.
472 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
475 =item cust_suspend_if_balance_over AMOUNT
477 Suspends the customer associated with this invoice if the total amount owed on
478 this invoice and all older invoices is greater than the specified amount.
480 Returns a list: an empty list on success or a list of errors.
484 sub cust_suspend_if_balance_over {
485 my( $self, $amount ) = ( shift, shift );
486 my $cust_main = $self->cust_main;
487 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
490 $cust_main->suspend(@_);
496 Depreciated. See the cust_credited method.
498 #Returns a list consisting of the total previous credited (see
499 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
500 #outstanding credits (FS::cust_credit objects).
506 croak "FS::cust_bill->cust_credit depreciated; see ".
507 "FS::cust_bill->cust_credit_bill";
510 #my @cust_credit = sort { $a->_date <=> $b->_date }
511 # grep { $_->credited != 0 && $_->_date < $self->_date }
512 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
514 #foreach (@cust_credit) { $total += $_->credited; }
515 #$total, @cust_credit;
520 Depreciated. See the cust_bill_pay method.
522 #Returns all payments (see L<FS::cust_pay>) for this invoice.
528 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
530 #sort { $a->_date <=> $b->_date }
531 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
537 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
540 sub cust_bill_pay_batch {
542 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
547 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
553 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
554 sort { $a->_date <=> $b->_date }
555 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
560 =item cust_credit_bill
562 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
568 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
569 sort { $a->_date <=> $b->_date }
570 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
574 sub cust_credit_bill {
575 shift->cust_credited(@_);
578 =item cust_bill_pay_pkgnum PKGNUM
580 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
581 with matching pkgnum.
585 sub cust_bill_pay_pkgnum {
586 my( $self, $pkgnum ) = @_;
587 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
588 sort { $a->_date <=> $b->_date }
589 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
595 =item cust_credited_pkgnum PKGNUM
597 =item cust_credit_bill_pkgnum PKGNUM
599 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
600 with matching pkgnum.
604 sub cust_credited_pkgnum {
605 my( $self, $pkgnum ) = @_;
606 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
607 sort { $a->_date <=> $b->_date }
608 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
614 sub cust_credit_bill_pkgnum {
615 shift->cust_credited_pkgnum(@_);
620 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
627 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
629 foreach (@taxlines) { $total += $_->setup; }
635 Returns the amount owed (still outstanding) on this invoice, which is charged
636 minus all payment applications (see L<FS::cust_bill_pay>) and credit
637 applications (see L<FS::cust_credit_bill>).
643 my $balance = $self->charged;
644 $balance -= $_->amount foreach ( $self->cust_bill_pay );
645 $balance -= $_->amount foreach ( $self->cust_credited );
646 $balance = sprintf( "%.2f", $balance);
647 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
652 my( $self, $pkgnum ) = @_;
654 #my $balance = $self->charged;
656 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
658 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
659 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
661 $balance = sprintf( "%.2f", $balance);
662 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
666 =item apply_payments_and_credits [ OPTION => VALUE ... ]
668 Applies unapplied payments and credits to this invoice.
670 A hash of optional arguments may be passed. Currently "manual" is supported.
671 If true, a payment receipt is sent instead of a statement when
672 'payment_receipt_email' configuration option is set.
674 If there is an error, returns the error, otherwise returns false.
678 sub apply_payments_and_credits {
679 my( $self, %options ) = @_;
681 local $SIG{HUP} = 'IGNORE';
682 local $SIG{INT} = 'IGNORE';
683 local $SIG{QUIT} = 'IGNORE';
684 local $SIG{TERM} = 'IGNORE';
685 local $SIG{TSTP} = 'IGNORE';
686 local $SIG{PIPE} = 'IGNORE';
688 my $oldAutoCommit = $FS::UID::AutoCommit;
689 local $FS::UID::AutoCommit = 0;
692 $self->select_for_update; #mutex
694 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
695 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
697 if ( $conf->exists('pkg-balances') ) {
698 # limit @payments & @credits to those w/ a pkgnum grepped from $self
699 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
700 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
701 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
704 while ( $self->owed > 0 and ( @payments || @credits ) ) {
707 if ( @payments && @credits ) {
709 #decide which goes first by weight of top (unapplied) line item
711 my @open_lineitems = $self->open_cust_bill_pkg;
714 max( map { $_->part_pkg->pay_weight || 0 }
719 my $max_credit_weight =
720 max( map { $_->part_pkg->credit_weight || 0 }
726 #if both are the same... payments first? it has to be something
727 if ( $max_pay_weight >= $max_credit_weight ) {
733 } elsif ( @payments ) {
735 } elsif ( @credits ) {
738 die "guru meditation #12 and 35";
742 if ( $app eq 'pay' ) {
744 my $payment = shift @payments;
745 $unapp_amount = $payment->unapplied;
746 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
747 $app->pkgnum( $payment->pkgnum )
748 if $conf->exists('pkg-balances') && $payment->pkgnum;
750 } elsif ( $app eq 'credit' ) {
752 my $credit = shift @credits;
753 $unapp_amount = $credit->credited;
754 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
755 $app->pkgnum( $credit->pkgnum )
756 if $conf->exists('pkg-balances') && $credit->pkgnum;
759 die "guru meditation #12 and 35";
763 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
764 warn "owed_pkgnum ". $app->pkgnum;
765 $owed = $self->owed_pkgnum($app->pkgnum);
769 next unless $owed > 0;
771 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
772 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
774 $app->invnum( $self->invnum );
776 my $error = $app->insert(%options);
778 $dbh->rollback if $oldAutoCommit;
779 return "Error inserting ". $app->table. " record: $error";
781 die $error if $error;
785 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
790 =item generate_email OPTION => VALUE ...
798 sender address, required
802 alternate template name, optional
806 text attachment arrayref, optional
810 email subject, optional
814 notice name instead of "Invoice", optional
818 Returns an argument list to be passed to L<FS::Misc::send_email>.
829 my $me = '[FS::cust_bill::generate_email]';
832 'from' => $args{'from'},
833 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
837 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
838 'template' => $args{'template'},
839 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
842 my $cust_main = $self->cust_main;
844 if (ref($args{'to'}) eq 'ARRAY') {
845 $return{'to'} = $args{'to'};
847 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
848 $cust_main->invoicing_list
852 if ( $conf->exists('invoice_html') ) {
854 warn "$me creating HTML/text multipart message"
857 $return{'nobody'} = 1;
859 my $alternative = build MIME::Entity
860 'Type' => 'multipart/alternative',
861 'Encoding' => '7bit',
862 'Disposition' => 'inline'
866 if ( $conf->exists('invoice_email_pdf')
867 and scalar($conf->config('invoice_email_pdf_note')) ) {
869 warn "$me using 'invoice_email_pdf_note' in multipart message"
871 $data = [ map { $_ . "\n" }
872 $conf->config('invoice_email_pdf_note')
877 warn "$me not using 'invoice_email_pdf_note' in multipart message"
879 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
880 $data = $args{'print_text'};
882 $data = [ $self->print_text(\%opt) ];
887 $alternative->attach(
888 'Type' => 'text/plain',
889 #'Encoding' => 'quoted-printable',
890 'Encoding' => '7bit',
892 'Disposition' => 'inline',
895 $args{'from'} =~ /\@([\w\.\-]+)/;
896 my $from = $1 || 'example.com';
897 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
900 my $agentnum = $cust_main->agentnum;
901 if ( defined($args{'template'}) && length($args{'template'})
902 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
905 $logo = 'logo_'. $args{'template'}. '.png';
909 my $image_data = $conf->config_binary( $logo, $agentnum);
911 my $image = build MIME::Entity
912 'Type' => 'image/png',
913 'Encoding' => 'base64',
914 'Data' => $image_data,
915 'Filename' => 'logo.png',
916 'Content-ID' => "<$content_id>",
919 $alternative->attach(
920 'Type' => 'text/html',
921 'Encoding' => 'quoted-printable',
922 'Data' => [ '<html>',
925 ' '. encode_entities($return{'subject'}),
928 ' <body bgcolor="#e8e8e8">',
929 $self->print_html({ 'cid'=>$content_id, %opt }),
933 'Disposition' => 'inline',
934 #'Filename' => 'invoice.pdf',
938 if ( $cust_main->email_csv_cdr ) {
940 push @otherparts, build MIME::Entity
941 'Type' => 'text/csv',
942 'Encoding' => '7bit',
943 'Data' => [ map { "$_\n" }
944 $self->call_details('prepend_billed_number' => 1)
946 'Disposition' => 'attachment',
947 'Filename' => 'usage-'. $self->invnum. '.csv',
952 if ( $conf->exists('invoice_email_pdf') ) {
957 # multipart/alternative
963 my $related = build MIME::Entity 'Type' => 'multipart/related',
964 'Encoding' => '7bit';
966 #false laziness w/Misc::send_email
967 $related->head->replace('Content-type',
969 '; boundary="'. $related->head->multipart_boundary. '"'.
970 '; type=multipart/alternative'
973 $related->add_part($alternative);
975 $related->add_part($image);
977 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
979 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
983 #no other attachment:
985 # multipart/alternative
990 $return{'content-type'} = 'multipart/related';
991 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
992 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
993 #$return{'disposition'} = 'inline';
999 if ( $conf->exists('invoice_email_pdf') ) {
1000 warn "$me creating PDF attachment"
1003 #mime parts arguments a la MIME::Entity->build().
1004 $return{'mimeparts'} = [
1005 { $self->mimebuild_pdf(\%opt) }
1009 if ( $conf->exists('invoice_email_pdf')
1010 and scalar($conf->config('invoice_email_pdf_note')) ) {
1012 warn "$me using 'invoice_email_pdf_note'"
1014 $return{'body'} = [ map { $_ . "\n" }
1015 $conf->config('invoice_email_pdf_note')
1020 warn "$me not using 'invoice_email_pdf_note'"
1022 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1023 $return{'body'} = $args{'print_text'};
1025 $return{'body'} = [ $self->print_text(\%opt) ];
1038 Returns a list suitable for passing to MIME::Entity->build(), representing
1039 this invoice as PDF attachment.
1046 'Type' => 'application/pdf',
1047 'Encoding' => 'base64',
1048 'Data' => [ $self->print_pdf(@_) ],
1049 'Disposition' => 'attachment',
1050 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1054 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1056 Sends this invoice to the destinations configured for this customer: sends
1057 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1059 Options can be passed as a hashref (recommended) or as a list of up to
1060 four values for templatename, agentnum, invoice_from and amount.
1062 I<template>, if specified, is the name of a suffix for alternate invoices.
1064 I<agentnum>, if specified, means that this invoice will only be sent for customers
1065 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1066 single agent) or an arrayref of agentnums.
1068 I<invoice_from>, if specified, overrides the default email invoice From: address.
1070 I<amount>, if specified, only sends the invoice if the total amount owed on this
1071 invoice and all older invoices is greater than the specified amount.
1073 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1077 sub queueable_send {
1080 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1081 or die "invalid invoice number: " . $opt{invnum};
1083 my @args = ( $opt{template}, $opt{agentnum} );
1084 push @args, $opt{invoice_from}
1085 if exists($opt{invoice_from}) && $opt{invoice_from};
1087 my $error = $self->send( @args );
1088 die $error if $error;
1095 my( $template, $invoice_from, $notice_name );
1097 my $balance_over = 0;
1101 $template = $opt->{'template'} || '';
1102 if ( $agentnums = $opt->{'agentnum'} ) {
1103 $agentnums = [ $agentnums ] unless ref($agentnums);
1105 $invoice_from = $opt->{'invoice_from'};
1106 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1107 $notice_name = $opt->{'notice_name'};
1109 $template = scalar(@_) ? shift : '';
1110 if ( scalar(@_) && $_[0] ) {
1111 $agentnums = ref($_[0]) ? shift : [ shift ];
1113 $invoice_from = shift if scalar(@_);
1114 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1117 return 'N/A' unless ! $agentnums
1118 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1121 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1123 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1124 $conf->config('invoice_from', $self->cust_main->agentnum );
1127 'template' => $template,
1128 'invoice_from' => $invoice_from,
1129 'notice_name' => ( $notice_name || 'Invoice' ),
1132 my @invoicing_list = $self->cust_main->invoicing_list;
1134 #$self->email_invoice(\%opt)
1136 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1138 #$self->print_invoice(\%opt)
1140 if grep { $_ eq 'POST' } @invoicing_list; #postal
1142 $self->fax_invoice(\%opt)
1143 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1149 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1151 Emails this invoice.
1153 Options can be passed as a hashref (recommended) or as a list of up to
1154 two values for templatename and invoice_from.
1156 I<template>, if specified, is the name of a suffix for alternate invoices.
1158 I<invoice_from>, if specified, overrides the default email invoice From: address.
1160 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1164 sub queueable_email {
1167 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1168 or die "invalid invoice number: " . $opt{invnum};
1170 my @args = ( $opt{template} );
1171 push @args, $opt{invoice_from}
1172 if exists($opt{invoice_from}) && $opt{invoice_from};
1174 my $error = $self->email( @args );
1175 die $error if $error;
1179 #sub email_invoice {
1183 my( $template, $invoice_from, $notice_name );
1186 $template = $opt->{'template'} || '';
1187 $invoice_from = $opt->{'invoice_from'};
1188 $notice_name = $opt->{'notice_name'} || 'Invoice';
1190 $template = scalar(@_) ? shift : '';
1191 $invoice_from = shift if scalar(@_);
1192 $notice_name = 'Invoice';
1195 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1196 $conf->config('invoice_from', $self->cust_main->agentnum );
1198 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1199 $self->cust_main->invoicing_list;
1201 #better to notify this person than silence
1202 @invoicing_list = ($invoice_from) unless @invoicing_list;
1204 my $subject = $self->email_subject($template);
1206 my $error = send_email(
1207 $self->generate_email(
1208 'from' => $invoice_from,
1209 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1210 'subject' => $subject,
1211 'template' => $template,
1212 'notice_name' => $notice_name,
1215 die "can't email invoice: $error\n" if $error;
1216 #die "$error\n" if $error;
1223 #my $template = scalar(@_) ? shift : '';
1226 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1229 my $cust_main = $self->cust_main;
1230 my $name = $cust_main->name;
1231 my $name_short = $cust_main->name_short;
1232 my $invoice_number = $self->invnum;
1233 my $invoice_date = $self->_date_pretty;
1235 eval qq("$subject");
1238 =item lpr_data HASHREF | [ TEMPLATE ]
1240 Returns the postscript or plaintext for this invoice as an arrayref.
1242 Options can be passed as a hashref (recommended) or as a single optional value
1245 I<template>, if specified, is the name of a suffix for alternate invoices.
1247 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1253 my( $template, $notice_name );
1256 $template = $opt->{'template'} || '';
1257 $notice_name = $opt->{'notice_name'} || 'Invoice';
1259 $template = scalar(@_) ? shift : '';
1260 $notice_name = 'Invoice';
1264 'template' => $template,
1265 'notice_name' => $notice_name,
1268 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1269 [ $self->$method( \%opt ) ];
1272 =item print HASHREF | [ TEMPLATE ]
1274 Prints this invoice.
1276 Options can be passed as a hashref (recommended) or as a single optional
1279 I<template>, if specified, is the name of a suffix for alternate invoices.
1281 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1285 #sub print_invoice {
1288 my( $template, $notice_name );
1291 $template = $opt->{'template'} || '';
1292 $notice_name = $opt->{'notice_name'} || 'Invoice';
1294 $template = scalar(@_) ? shift : '';
1295 $notice_name = 'Invoice';
1299 'template' => $template,
1300 'notice_name' => $notice_name,
1303 do_print $self->lpr_data(\%opt);
1306 =item fax_invoice HASHREF | [ TEMPLATE ]
1310 Options can be passed as a hashref (recommended) or as a single optional
1313 I<template>, if specified, is the name of a suffix for alternate invoices.
1315 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1321 my( $template, $notice_name );
1324 $template = $opt->{'template'} || '';
1325 $notice_name = $opt->{'notice_name'} || 'Invoice';
1327 $template = scalar(@_) ? shift : '';
1328 $notice_name = 'Invoice';
1331 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1332 unless $conf->exists('invoice_latex');
1334 my $dialstring = $self->cust_main->getfield('fax');
1338 'template' => $template,
1339 'notice_name' => $notice_name,
1342 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1343 'dialstring' => $dialstring,
1345 die $error if $error;
1349 =item ftp_invoice [ TEMPLATENAME ]
1351 Sends this invoice data via FTP.
1353 TEMPLATENAME is unused?
1359 my $template = scalar(@_) ? shift : '';
1362 'protocol' => 'ftp',
1363 'server' => $conf->config('cust_bill-ftpserver'),
1364 'username' => $conf->config('cust_bill-ftpusername'),
1365 'password' => $conf->config('cust_bill-ftppassword'),
1366 'dir' => $conf->config('cust_bill-ftpdir'),
1367 'format' => $conf->config('cust_bill-ftpformat'),
1371 =item spool_invoice [ TEMPLATENAME ]
1373 Spools this invoice data (see L<FS::spool_csv>)
1375 TEMPLATENAME is unused?
1381 my $template = scalar(@_) ? shift : '';
1384 'format' => $conf->config('cust_bill-spoolformat'),
1385 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1389 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1391 Like B<send>, but only sends the invoice if it is the newest open invoice for
1396 sub send_if_newest {
1401 grep { $_->owed > 0 }
1402 qsearch('cust_bill', {
1403 'custnum' => $self->custnum,
1404 #'_date' => { op=>'>', value=>$self->_date },
1405 'invnum' => { op=>'>', value=>$self->invnum },
1412 =item send_csv OPTION => VALUE, ...
1414 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1418 protocol - currently only "ftp"
1424 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1425 and YYMMDDHHMMSS is a timestamp.
1427 See L</print_csv> for a description of the output format.
1432 my($self, %opt) = @_;
1436 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1437 mkdir $spooldir, 0700 unless -d $spooldir;
1439 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1440 my $file = "$spooldir/$tracctnum.csv";
1442 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1444 open(CSV, ">$file") or die "can't open $file: $!";
1452 if ( $opt{protocol} eq 'ftp' ) {
1453 eval "use Net::FTP;";
1455 $net = Net::FTP->new($opt{server}) or die @$;
1457 die "unknown protocol: $opt{protocol}";
1460 $net->login( $opt{username}, $opt{password} )
1461 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1463 $net->binary or die "can't set binary mode";
1465 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1467 $net->put($file) or die "can't put $file: $!";
1477 Spools CSV invoice data.
1483 =item format - 'default' or 'billco'
1485 =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>).
1487 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1489 =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.
1496 my($self, %opt) = @_;
1498 my $cust_main = $self->cust_main;
1500 if ( $opt{'dest'} ) {
1501 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1502 $cust_main->invoicing_list;
1503 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1504 || ! keys %invoicing_list;
1507 if ( $opt{'balanceover'} ) {
1509 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1512 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1513 mkdir $spooldir, 0700 unless -d $spooldir;
1515 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1519 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1520 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1523 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1525 open(CSV, ">>$file") or die "can't open $file: $!";
1526 flock(CSV, LOCK_EX);
1531 if ( lc($opt{'format'}) eq 'billco' ) {
1533 flock(CSV, LOCK_UN);
1538 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1541 open(CSV,">>$file") or die "can't open $file: $!";
1542 flock(CSV, LOCK_EX);
1548 flock(CSV, LOCK_UN);
1555 =item print_csv OPTION => VALUE, ...
1557 Returns CSV data for this invoice.
1561 format - 'default' or 'billco'
1563 Returns a list consisting of two scalars. The first is a single line of CSV
1564 header information for this invoice. The second is one or more lines of CSV
1565 detail information for this invoice.
1567 If I<format> is not specified or "default", the fields of the CSV file are as
1570 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1574 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1576 B<record_type> is C<cust_bill> for the initial header line only. The
1577 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1578 fields are filled in.
1580 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1581 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1584 =item invnum - invoice number
1586 =item custnum - customer number
1588 =item _date - invoice date
1590 =item charged - total invoice amount
1592 =item first - customer first name
1594 =item last - customer first name
1596 =item company - company name
1598 =item address1 - address line 1
1600 =item address2 - address line 1
1610 =item pkg - line item description
1612 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1614 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1616 =item sdate - start date for recurring fee
1618 =item edate - end date for recurring fee
1622 If I<format> is "billco", the fields of the header CSV file are as follows:
1624 +-------------------------------------------------------------------+
1625 | FORMAT HEADER FILE |
1626 |-------------------------------------------------------------------|
1627 | Field | Description | Name | Type | Width |
1628 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1629 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1630 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1631 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1632 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1633 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1634 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1635 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1636 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1637 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1638 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1639 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1640 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1641 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1642 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1643 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1644 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1645 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1646 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1647 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1648 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1649 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1650 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1651 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1652 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1653 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1654 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1655 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1656 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1657 +-------+-------------------------------+------------+------+-------+
1659 If I<format> is "billco", the fields of the detail CSV file are as follows:
1661 FORMAT FOR DETAIL FILE
1663 Field | Description | Name | Type | Width
1664 1 | N/A-Leave Empty | RC | CHAR | 2
1665 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1666 3 | Account Number | TRACCTNUM | CHAR | 15
1667 4 | Invoice Number | TRINVOICE | CHAR | 15
1668 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1669 6 | Transaction Detail | DETAILS | CHAR | 100
1670 7 | Amount | AMT | NUM* | 9
1671 8 | Line Format Control** | LNCTRL | CHAR | 2
1672 9 | Grouping Code | GROUP | CHAR | 2
1673 10 | User Defined | ACCT CODE | CHAR | 15
1678 my($self, %opt) = @_;
1680 eval "use Text::CSV_XS";
1683 my $cust_main = $self->cust_main;
1685 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1687 if ( lc($opt{'format'}) eq 'billco' ) {
1690 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1692 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1694 my( $previous_balance, @unused ) = $self->previous; #previous balance
1696 my $pmt_cr_applied = 0;
1697 $pmt_cr_applied += $_->{'amount'}
1698 foreach ( $self->_items_payments, $self->_items_credits ) ;
1700 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1703 '', # 1 | N/A-Leave Empty CHAR 2
1704 '', # 2 | N/A-Leave Empty CHAR 15
1705 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1706 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1707 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1708 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1709 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1710 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1711 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1712 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1713 '', # 10 | Ancillary Billing Information CHAR 30
1714 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1715 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1718 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1721 $duedate, # 14 | Bill Due Date CHAR 10
1723 $previous_balance, # 15 | Previous Balance NUM* 9
1724 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1725 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1726 $totaldue, # 18 | Total Amt Due NUM* 9
1727 $totaldue, # 19 | Total Amt Due NUM* 9
1728 '', # 20 | 30 Day Aging NUM* 9
1729 '', # 21 | 60 Day Aging NUM* 9
1730 '', # 22 | 90 Day Aging NUM* 9
1731 'N', # 23 | Y/N CHAR 1
1732 '', # 24 | Remittance automation CHAR 100
1733 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1734 $self->custnum, # 26 | Customer Reference Number CHAR 15
1735 '0', # 27 | Federal Tax*** NUM* 9
1736 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1737 '0', # 29 | Other Taxes & Fees*** NUM* 9
1746 time2str("%x", $self->_date),
1747 sprintf("%.2f", $self->charged),
1748 ( map { $cust_main->getfield($_) }
1749 qw( first last company address1 address2 city state zip country ) ),
1751 ) or die "can't create csv";
1754 my $header = $csv->string. "\n";
1757 if ( lc($opt{'format'}) eq 'billco' ) {
1760 foreach my $item ( $self->_items_pkg ) {
1763 '', # 1 | N/A-Leave Empty CHAR 2
1764 '', # 2 | N/A-Leave Empty CHAR 15
1765 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1766 $self->invnum, # 4 | Invoice Number CHAR 15
1767 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1768 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1769 $item->{'amount'}, # 7 | Amount NUM* 9
1770 '', # 8 | Line Format Control** CHAR 2
1771 '', # 9 | Grouping Code CHAR 2
1772 '', # 10 | User Defined CHAR 15
1775 $detail .= $csv->string. "\n";
1781 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1783 my($pkg, $setup, $recur, $sdate, $edate);
1784 if ( $cust_bill_pkg->pkgnum ) {
1786 ($pkg, $setup, $recur, $sdate, $edate) = (
1787 $cust_bill_pkg->part_pkg->pkg,
1788 ( $cust_bill_pkg->setup != 0
1789 ? sprintf("%.2f", $cust_bill_pkg->setup )
1791 ( $cust_bill_pkg->recur != 0
1792 ? sprintf("%.2f", $cust_bill_pkg->recur )
1794 ( $cust_bill_pkg->sdate
1795 ? time2str("%x", $cust_bill_pkg->sdate)
1797 ($cust_bill_pkg->edate
1798 ?time2str("%x", $cust_bill_pkg->edate)
1802 } else { #pkgnum tax
1803 next unless $cust_bill_pkg->setup != 0;
1804 $pkg = $cust_bill_pkg->desc;
1805 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1806 ( $sdate, $edate ) = ( '', '' );
1812 ( map { '' } (1..11) ),
1813 ($pkg, $setup, $recur, $sdate, $edate)
1814 ) or die "can't create csv";
1816 $detail .= $csv->string. "\n";
1822 ( $header, $detail );
1828 Pays this invoice with a compliemntary payment. If there is an error,
1829 returns the error, otherwise returns false.
1835 my $cust_pay = new FS::cust_pay ( {
1836 'invnum' => $self->invnum,
1837 'paid' => $self->owed,
1840 'payinfo' => $self->cust_main->payinfo,
1848 Attempts to pay this invoice with a credit card payment via a
1849 Business::OnlinePayment realtime gateway. See
1850 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1851 for supported processors.
1857 $self->realtime_bop( 'CC', @_ );
1862 Attempts to pay this invoice with an electronic check (ACH) payment via a
1863 Business::OnlinePayment realtime gateway. See
1864 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1865 for supported processors.
1871 $self->realtime_bop( 'ECHECK', @_ );
1876 Attempts to pay this invoice with phone bill (LEC) payment via a
1877 Business::OnlinePayment realtime gateway. See
1878 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1879 for supported processors.
1885 $self->realtime_bop( 'LEC', @_ );
1889 my( $self, $method ) = @_;
1891 my $cust_main = $self->cust_main;
1892 my $balance = $cust_main->balance;
1893 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1894 $amount = sprintf("%.2f", $amount);
1895 return "not run (balance $balance)" unless $amount > 0;
1897 my $description = 'Internet Services';
1898 if ( $conf->exists('business-onlinepayment-description') ) {
1899 my $dtempl = $conf->config('business-onlinepayment-description');
1901 my $agent_obj = $cust_main->agent
1902 or die "can't retreive agent for $cust_main (agentnum ".
1903 $cust_main->agentnum. ")";
1904 my $agent = $agent_obj->agent;
1905 my $pkgs = join(', ',
1906 map { $_->part_pkg->pkg }
1907 grep { $_->pkgnum } $self->cust_bill_pkg
1909 $description = eval qq("$dtempl");
1912 $cust_main->realtime_bop($method, $amount,
1913 'description' => $description,
1914 'invnum' => $self->invnum,
1915 #this didn't do what we want, it just calls apply_payments_and_credits
1917 'apply_to_invoice' => 1,
1919 #this changes application behavior: auto payments
1920 #triggered against a specific invoice are now applied
1921 #to that invoice instead of oldest open.
1927 =item batch_card OPTION => VALUE...
1929 Adds a payment for this invoice to the pending credit card batch (see
1930 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1931 runs the payment using a realtime gateway.
1936 my ($self, %options) = @_;
1937 my $cust_main = $self->cust_main;
1939 $options{invnum} = $self->invnum;
1941 $cust_main->batch_card(%options);
1944 sub _agent_template {
1946 $self->cust_main->agent_template;
1949 sub _agent_invoice_from {
1951 $self->cust_main->agent_invoice_from;
1954 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1956 Returns an text invoice, as a list of lines.
1958 Options can be passed as a hashref (recommended) or as a list of time, template
1959 and then any key/value pairs for any other options.
1961 I<time>, if specified, is used to control the printing of overdue messages. The
1962 default is now. It isn't the date of the invoice; that's the `_date' field.
1963 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1964 L<Time::Local> and L<Date::Parse> for conversion functions.
1966 I<template>, if specified, is the name of a suffix for alternate invoices.
1968 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1974 my( $today, $template, %opt );
1976 %opt = %{ shift() };
1977 $today = delete($opt{'time'}) || '';
1978 $template = delete($opt{template}) || '';
1980 ( $today, $template, %opt ) = @_;
1983 my %params = ( 'format' => 'template' );
1984 $params{'time'} = $today if $today;
1985 $params{'template'} = $template if $template;
1986 $params{$_} = $opt{$_}
1987 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1989 $self->print_generic( %params );
1992 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1994 Internal method - returns a filename of a filled-in LaTeX template for this
1995 invoice (Note: add ".tex" to get the actual filename), and a filename of
1996 an associated logo (with the .eps extension included).
1998 See print_ps and print_pdf for methods that return PostScript and PDF output.
2000 Options can be passed as a hashref (recommended) or as a list of time, template
2001 and then any key/value pairs for any other options.
2003 I<time>, if specified, is used to control the printing of overdue messages. The
2004 default is now. It isn't the date of the invoice; that's the `_date' field.
2005 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2006 L<Time::Local> and L<Date::Parse> for conversion functions.
2008 I<template>, if specified, is the name of a suffix for alternate invoices.
2010 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2016 my( $today, $template, %opt );
2018 %opt = %{ shift() };
2019 $today = delete($opt{'time'}) || '';
2020 $template = delete($opt{template}) || '';
2022 ( $today, $template, %opt ) = @_;
2025 my %params = ( 'format' => 'latex' );
2026 $params{'time'} = $today if $today;
2027 $params{'template'} = $template if $template;
2028 $params{$_} = $opt{$_}
2029 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2031 $template ||= $self->_agent_template;
2033 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2034 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2038 ) or die "can't open temp file: $!\n";
2040 my $agentnum = $self->cust_main->agentnum;
2042 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2043 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2044 or die "can't write temp file: $!\n";
2046 print $lh $conf->config_binary('logo.eps', $agentnum)
2047 or die "can't write temp file: $!\n";
2050 $params{'logo_file'} = $lh->filename;
2052 my @filled_in = $self->print_generic( %params );
2054 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2058 ) or die "can't open temp file: $!\n";
2059 print $fh join('', @filled_in );
2062 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2063 return ($1, $params{'logo_file'});
2067 =item print_generic OPTION => VALUE ...
2069 Internal method - returns a filled-in template for this invoice as a scalar.
2071 See print_ps and print_pdf for methods that return PostScript and PDF output.
2073 Non optional options include
2074 format - latex, html, template
2076 Optional options include
2078 template - a value used as a suffix for a configuration template
2080 time - a value used to control the printing of overdue messages. The
2081 default is now. It isn't the date of the invoice; that's the `_date' field.
2082 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2083 L<Time::Local> and L<Date::Parse> for conversion functions.
2087 unsquelch_cdr - overrides any per customer cdr squelching when true
2089 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2093 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2094 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2095 # yes: fixed width (dot matrix) text printing will be borked
2098 my( $self, %params ) = @_;
2099 my $today = $params{today} ? $params{today} : time;
2100 warn "$me print_generic called on $self with suffix $params{template}\n"
2103 my $format = $params{format};
2104 die "Unknown format: $format"
2105 unless $format =~ /^(latex|html|template)$/;
2107 my $cust_main = $self->cust_main;
2108 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2109 unless $cust_main->payname
2110 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2112 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2113 'html' => [ '<%=', '%>' ],
2114 'template' => [ '{', '}' ],
2117 #create the template
2118 my $template = $params{template} ? $params{template} : $self->_agent_template;
2119 my $templatefile = "invoice_$format";
2120 $templatefile .= "_$template"
2121 if length($template);
2122 my @invoice_template = map "$_\n", $conf->config($templatefile)
2123 or die "cannot load config data $templatefile";
2126 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2127 #change this to a die when the old code is removed
2128 warn "old-style invoice template $templatefile; ".
2129 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2130 $old_latex = 'true';
2131 @invoice_template = _translate_old_latex_format(@invoice_template);
2134 my $text_template = new Text::Template(
2136 SOURCE => \@invoice_template,
2137 DELIMITERS => $delimiters{$format},
2140 $text_template->compile()
2141 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2144 # additional substitution could possibly cause breakage in existing templates
2145 my %convert_maps = (
2147 'notes' => sub { map "$_", @_ },
2148 'footer' => sub { map "$_", @_ },
2149 'smallfooter' => sub { map "$_", @_ },
2150 'returnaddress' => sub { map "$_", @_ },
2151 'coupon' => sub { map "$_", @_ },
2152 'summary' => sub { map "$_", @_ },
2158 s/%%(.*)$/<!-- $1 -->/g;
2159 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2160 s/\\begin\{enumerate\}/<ol>/g;
2162 s/\\end\{enumerate\}/<\/ol>/g;
2163 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2172 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2174 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2179 s/\\\\\*?\s*$/<BR>/;
2180 s/\\hyphenation\{[\w\s\-]+}//;
2185 'coupon' => sub { "" },
2186 'summary' => sub { "" },
2193 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2194 s/\\begin\{enumerate\}//g;
2196 s/\\end\{enumerate\}//g;
2197 s/\\textbf\{(.*)\}/$1/g;
2204 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2206 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2211 s/\\\\\*?\s*$/\n/; # dubious
2212 s/\\hyphenation\{[\w\s\-]+}//;
2216 'coupon' => sub { "" },
2217 'summary' => sub { "" },
2222 # hashes for differing output formats
2223 my %nbsps = ( 'latex' => '~',
2224 'html' => '', # '&nbps;' would be nice
2225 'template' => '', # not used
2227 my $nbsp = $nbsps{$format};
2229 my %escape_functions = ( 'latex' => \&_latex_escape,
2230 'html' => \&encode_entities,
2231 'template' => sub { shift },
2233 my $escape_function = $escape_functions{$format};
2235 my %date_formats = ( 'latex' => '%b %o, %Y',
2236 'html' => '%b %o, %Y',
2239 my $date_format = $date_formats{$format};
2241 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2243 'html' => sub { return '<b>'. shift(). '</b>'
2245 'template' => sub { shift },
2247 my $embolden_function = $embolden_functions{$format};
2250 # generate template variables
2253 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2257 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2263 $returnaddress = join("\n",
2264 $conf->config_orbase("invoice_${format}returnaddress", $template)
2267 } elsif ( grep /\S/,
2268 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2270 my $convert_map = $convert_maps{$format}{'returnaddress'};
2273 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2278 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2280 my $convert_map = $convert_maps{$format}{'returnaddress'};
2281 $returnaddress = join( "\n", &$convert_map(
2282 map { s/( {2,})/'~' x length($1)/eg;
2286 ( $conf->config('company_name', $self->cust_main->agentnum),
2287 $conf->config('company_address', $self->cust_main->agentnum),
2294 my $warning = "Couldn't find a return address; ".
2295 "do you need to set the company_address configuration value?";
2297 $returnaddress = $nbsp;
2298 #$returnaddress = $warning;
2302 my %invoice_data = (
2305 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2306 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2307 'returnaddress' => $returnaddress,
2308 'agent' => &$escape_function($cust_main->agent->agent),
2311 'invnum' => $self->invnum,
2312 'date' => time2str($date_format, $self->_date),
2313 'today' => time2str('%b %o, %Y', $today),
2314 'terms' => $self->terms,
2315 'template' => $template, #params{'template'},
2316 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2317 'current_charges' => sprintf("%.2f", $self->charged),
2318 'duedate' => $self->due_date2str($rdate_format), #date_format?
2321 'custnum' => $cust_main->display_custnum,
2322 'agent_custid' => &$escape_function($cust_main->agent_custid),
2323 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2324 payname company address1 address2 city state zip fax
2328 'ship_enable' => $conf->exists('invoice-ship_address'),
2329 'unitprices' => $conf->exists('invoice-unitprice'),
2330 'smallernotes' => $conf->exists('invoice-smallernotes'),
2331 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2333 # better hang on to conf_dir for a while (for old templates)
2334 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2336 #these are only used when doing paged plaintext
2342 $invoice_data{finance_section} = '';
2343 if ( $conf->config('finance_pkgclass') ) {
2345 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2346 $invoice_data{finance_section} = $pkg_class->categoryname;
2348 $invoice_data{finance_amount} = '0.00';
2350 my $countrydefault = $conf->config('countrydefault') || 'US';
2351 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2352 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2353 my $method = $prefix.$_;
2354 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2356 $invoice_data{'ship_country'} = ''
2357 if ( $invoice_data{'ship_country'} eq $countrydefault );
2359 $invoice_data{'cid'} = $params{'cid'}
2362 if ( $cust_main->country eq $countrydefault ) {
2363 $invoice_data{'country'} = '';
2365 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2369 $invoice_data{'address'} = \@address;
2371 $cust_main->payname.
2372 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2373 ? " (P.O. #". $cust_main->payinfo. ")"
2377 push @address, $cust_main->company
2378 if $cust_main->company;
2379 push @address, $cust_main->address1;
2380 push @address, $cust_main->address2
2381 if $cust_main->address2;
2383 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2384 push @address, $invoice_data{'country'}
2385 if $invoice_data{'country'};
2387 while (scalar(@address) < 5);
2389 $invoice_data{'logo_file'} = $params{'logo_file'}
2390 if $params{'logo_file'};
2392 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2393 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2394 #my $balance_due = $self->owed + $pr_total - $cr_total;
2395 my $balance_due = $self->owed + $pr_total;
2396 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2397 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2398 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2399 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2401 my $agentnum = $self->cust_main->agentnum;
2403 my $summarypage = '';
2404 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2407 $invoice_data{'summarypage'} = $summarypage;
2409 #do variable substitution in notes, footer, smallfooter
2410 foreach my $include (qw( notes footer smallfooter coupon )) {
2412 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2415 if ( $conf->exists($inc_file, $agentnum)
2416 && length( $conf->config($inc_file, $agentnum) ) ) {
2418 @inc_src = $conf->config($inc_file, $agentnum);
2422 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2424 my $convert_map = $convert_maps{$format}{$include};
2426 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2427 s/--\@\]/$delimiters{$format}[1]/g;
2430 &$convert_map( $conf->config($inc_file, $agentnum) );
2434 my $inc_tt = new Text::Template (
2436 SOURCE => [ map "$_\n", @inc_src ],
2437 DELIMITERS => $delimiters{$format},
2438 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2440 unless ( $inc_tt->compile() ) {
2441 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2442 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2446 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2448 $invoice_data{$include} =~ s/\n+$//
2449 if ($format eq 'latex');
2452 $invoice_data{'po_line'} =
2453 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2454 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2457 my %money_chars = ( 'latex' => '',
2458 'html' => $conf->config('money_char') || '$',
2461 my $money_char = $money_chars{$format};
2463 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2464 'html' => $conf->config('money_char') || '$',
2467 my $other_money_char = $other_money_chars{$format};
2468 $invoice_data{'dollar'} = $other_money_char;
2470 my @detail_items = ();
2471 my @total_items = ();
2475 $invoice_data{'detail_items'} = \@detail_items;
2476 $invoice_data{'total_items'} = \@total_items;
2477 $invoice_data{'buf'} = \@buf;
2478 $invoice_data{'sections'} = \@sections;
2480 my $previous_section = { 'description' => 'Previous Charges',
2481 'subtotal' => $other_money_char.
2482 sprintf('%.2f', $pr_total),
2483 'summarized' => $summarypage ? 'Y' : '',
2485 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2486 join(' / ', map { $cust_main->balance_date_range(@$_) }
2487 $self->_prior_month30s
2489 if $conf->exists('invoice_include_aging');
2492 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2493 'subtotal' => $taxtotal, # adjusted below
2494 'summarized' => $summarypage ? 'Y' : '',
2496 my $tax_weight = _pkg_category($tax_section->{description})
2497 ? _pkg_category($tax_section->{description})->weight
2499 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2500 $tax_section->{'sort_weight'} = $tax_weight;
2503 my $adjusttotal = 0;
2504 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2505 'subtotal' => 0, # adjusted below
2506 'summarized' => $summarypage ? 'Y' : '',
2508 my $adjust_weight = _pkg_category($adjust_section->{description})
2509 ? _pkg_category($adjust_section->{description})->weight
2511 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2512 $adjust_section->{'sort_weight'} = $adjust_weight;
2514 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2515 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2516 my $late_sections = [];
2517 my $extra_sections = [];
2518 my $extra_lines = ();
2519 if ( $multisection ) {
2520 ($extra_sections, $extra_lines) =
2521 $self->_items_extra_usage_sections($escape_function, $format)
2522 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2524 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2526 push @detail_items, @$extra_lines if $extra_lines;
2528 $self->_items_sections( $late_sections, # this could stand a refactor
2534 if ($conf->exists('svc_phone_sections')) {
2535 my ($phone_sections, $phone_lines) =
2536 $self->_items_svc_phone_sections($escape_function, $format);
2537 push @{$late_sections}, @$phone_sections;
2538 push @detail_items, @$phone_lines;
2541 push @sections, { 'description' => '', 'subtotal' => '' };
2544 unless ( $conf->exists('disable_previous_balance')
2545 || $conf->exists('previous_balance-summary_only')
2549 foreach my $line_item ( $self->_items_previous ) {
2552 ext_description => [],
2554 $detail->{'ref'} = $line_item->{'pkgnum'};
2555 $detail->{'quantity'} = 1;
2556 $detail->{'section'} = $previous_section;
2557 $detail->{'description'} = &$escape_function($line_item->{'description'});
2558 if ( exists $line_item->{'ext_description'} ) {
2559 @{$detail->{'ext_description'}} = map {
2560 &$escape_function($_);
2561 } @{$line_item->{'ext_description'}};
2563 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2564 $line_item->{'amount'};
2565 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2567 push @detail_items, $detail;
2568 push @buf, [ $detail->{'description'},
2569 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2575 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2576 push @buf, ['','-----------'];
2577 push @buf, [ 'Total Previous Balance',
2578 $money_char. sprintf("%10.2f", $pr_total) ];
2582 foreach my $section (@sections, @$late_sections) {
2584 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2585 if ( $invoice_data{finance_section} &&
2586 $section->{'description'} eq $invoice_data{finance_section} );
2588 $section->{'subtotal'} = $other_money_char.
2589 sprintf('%.2f', $section->{'subtotal'})
2592 # begin some normalization
2593 $section->{'amount'} = $section->{'subtotal'}
2597 if ( $section->{'description'} ) {
2598 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2603 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2605 $options{'section'} = $section if $multisection;
2606 $options{'format'} = $format;
2607 $options{'escape_function'} = $escape_function;
2608 $options{'format_function'} = sub { () } unless $unsquelched;
2609 $options{'unsquelched'} = $unsquelched;
2610 $options{'summary_page'} = $summarypage;
2611 $options{'skip_usage'} =
2612 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2613 $options{'multilocation'} = $multilocation;
2615 foreach my $line_item ( $self->_items_pkg(%options) ) {
2617 ext_description => [],
2619 $detail->{'ref'} = $line_item->{'pkgnum'};
2620 $detail->{'quantity'} = $line_item->{'quantity'};
2621 $detail->{'section'} = $section;
2622 $detail->{'description'} = &$escape_function($line_item->{'description'});
2623 if ( exists $line_item->{'ext_description'} ) {
2624 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2626 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2627 $line_item->{'amount'};
2628 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2629 $line_item->{'unit_amount'};
2630 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2632 push @detail_items, $detail;
2633 push @buf, ( [ $detail->{'description'},
2634 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2636 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2640 if ( $section->{'description'} ) {
2641 push @buf, ( ['','-----------'],
2642 [ $section->{'description'}. ' sub-total',
2643 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2652 $invoice_data{current_less_finance} =
2653 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2655 if ( $multisection && !$conf->exists('disable_previous_balance')
2656 || $conf->exists('previous_balance-summary_only') )
2658 unshift @sections, $previous_section if $pr_total;
2661 foreach my $tax ( $self->_items_tax ) {
2663 $taxtotal += $tax->{'amount'};
2665 my $description = &$escape_function( $tax->{'description'} );
2666 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2668 if ( $multisection ) {
2670 my $money = $old_latex ? '' : $money_char;
2671 push @detail_items, {
2672 ext_description => [],
2675 description => $description,
2676 amount => $money. $amount,
2678 section => $tax_section,
2683 push @total_items, {
2684 'total_item' => $description,
2685 'total_amount' => $other_money_char. $amount,
2690 push @buf,[ $description,
2691 $money_char. $amount,
2698 $total->{'total_item'} = 'Sub-total';
2699 $total->{'total_amount'} =
2700 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2702 if ( $multisection ) {
2703 $tax_section->{'subtotal'} = $other_money_char.
2704 sprintf('%.2f', $taxtotal);
2705 $tax_section->{'pretotal'} = 'New charges sub-total '.
2706 $total->{'total_amount'};
2707 push @sections, $tax_section if $taxtotal;
2709 unshift @total_items, $total;
2712 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2714 push @buf,['','-----------'];
2715 push @buf,[( $conf->exists('disable_previous_balance')
2717 : 'Total New Charges'
2719 $money_char. sprintf("%10.2f",$self->charged) ];
2724 $total->{'total_item'} = &$embolden_function('Total');
2725 $total->{'total_amount'} =
2726 &$embolden_function(
2729 $self->charged + ( $conf->exists('disable_previous_balance')
2735 if ( $multisection ) {
2736 if ( $adjust_section->{'sort_weight'} ) {
2737 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2738 sprintf("%.2f", ($self->billing_balance || 0) );
2740 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2741 sprintf('%.2f', $self->charged );
2744 push @total_items, $total;
2746 push @buf,['','-----------'];
2747 push @buf,['Total Charges',
2749 sprintf( '%10.2f', $self->charged +
2750 ( $conf->exists('disable_previous_balance')
2759 unless ( $conf->exists('disable_previous_balance') ) {
2760 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2763 my $credittotal = 0;
2764 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2767 $total->{'total_item'} = &$escape_function($credit->{'description'});
2768 $credittotal += $credit->{'amount'};
2769 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2770 $adjusttotal += $credit->{'amount'};
2771 if ( $multisection ) {
2772 my $money = $old_latex ? '' : $money_char;
2773 push @detail_items, {
2774 ext_description => [],
2777 description => &$escape_function($credit->{'description'}),
2778 amount => $money. $credit->{'amount'},
2780 section => $adjust_section,
2783 push @total_items, $total;
2787 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2790 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2791 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2795 my $paymenttotal = 0;
2796 foreach my $payment ( $self->_items_payments ) {
2798 $total->{'total_item'} = &$escape_function($payment->{'description'});
2799 $paymenttotal += $payment->{'amount'};
2800 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2801 $adjusttotal += $payment->{'amount'};
2802 if ( $multisection ) {
2803 my $money = $old_latex ? '' : $money_char;
2804 push @detail_items, {
2805 ext_description => [],
2808 description => &$escape_function($payment->{'description'}),
2809 amount => $money. $payment->{'amount'},
2811 section => $adjust_section,
2814 push @total_items, $total;
2816 push @buf, [ $payment->{'description'},
2817 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2820 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2822 if ( $multisection ) {
2823 $adjust_section->{'subtotal'} = $other_money_char.
2824 sprintf('%.2f', $adjusttotal);
2825 push @sections, $adjust_section
2826 unless $adjust_section->{sort_weight};
2831 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2832 $total->{'total_amount'} =
2833 &$embolden_function(
2834 $other_money_char. sprintf('%.2f', $summarypage
2836 $self->billing_balance
2837 : $self->owed + $pr_total
2840 if ( $multisection && !$adjust_section->{sort_weight} ) {
2841 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2842 $total->{'total_amount'};
2844 push @total_items, $total;
2846 push @buf,['','-----------'];
2847 push @buf,[$self->balance_due_msg, $money_char.
2848 sprintf("%10.2f", $balance_due ) ];
2852 if ( $multisection ) {
2853 if ($conf->exists('svc_phone_sections')) {
2855 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2856 $total->{'total_amount'} =
2857 &$embolden_function(
2858 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2860 my $last_section = pop @sections;
2861 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2862 $total->{'total_amount'};
2863 push @sections, $last_section;
2865 push @sections, @$late_sections
2869 my @includelist = ();
2870 push @includelist, 'summary' if $summarypage;
2871 foreach my $include ( @includelist ) {
2873 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2876 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2878 @inc_src = $conf->config($inc_file, $agentnum);
2882 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2884 my $convert_map = $convert_maps{$format}{$include};
2886 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2887 s/--\@\]/$delimiters{$format}[1]/g;
2890 &$convert_map( $conf->config($inc_file, $agentnum) );
2894 my $inc_tt = new Text::Template (
2896 SOURCE => [ map "$_\n", @inc_src ],
2897 DELIMITERS => $delimiters{$format},
2898 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2900 unless ( $inc_tt->compile() ) {
2901 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2902 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2906 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2908 $invoice_data{$include} =~ s/\n+$//
2909 if ($format eq 'latex');
2914 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2915 /invoice_lines\((\d*)\)/;
2916 $invoice_lines += $1 || scalar(@buf);
2919 die "no invoice_lines() functions in template?"
2920 if ( $format eq 'template' && !$wasfunc );
2922 if ($format eq 'template') {
2924 if ( $invoice_lines ) {
2925 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2926 $invoice_data{'total_pages'}++
2927 if scalar(@buf) % $invoice_lines;
2930 #setup subroutine for the template
2931 sub FS::cust_bill::_template::invoice_lines {
2932 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2934 scalar(@FS::cust_bill::_template::buf)
2935 ? shift @FS::cust_bill::_template::buf
2944 push @collect, split("\n",
2945 $text_template->fill_in( HASH => \%invoice_data,
2946 PACKAGE => 'FS::cust_bill::_template'
2949 $FS::cust_bill::_template::page++;
2951 map "$_\n", @collect;
2953 warn "filling in template for invoice ". $self->invnum. "\n"
2955 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2958 $text_template->fill_in(HASH => \%invoice_data);
2962 # helper routine for generating date ranges
2963 sub _prior_month30s {
2966 [ 1, 2592000 ], # 0-30 days ago
2967 [ 2592000, 5184000 ], # 30-60 days ago
2968 [ 5184000, 7776000 ], # 60-90 days ago
2969 [ 7776000, 0 ], # 90+ days ago
2972 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2973 $_->[1] ? $self->_date - $_->[1] - 1 : '',
2978 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2980 Returns an postscript invoice, as a scalar.
2982 Options can be passed as a hashref (recommended) or as a list of time, template
2983 and then any key/value pairs for any other options.
2985 I<time> an optional value used to control the printing of overdue messages. The
2986 default is now. It isn't the date of the invoice; that's the `_date' field.
2987 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2988 L<Time::Local> and L<Date::Parse> for conversion functions.
2990 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2997 my ($file, $lfile) = $self->print_latex(@_);
2998 my $ps = generate_ps($file);
3004 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3006 Returns an PDF invoice, as a scalar.
3008 Options can be passed as a hashref (recommended) or as a list of time, template
3009 and then any key/value pairs for any other options.
3011 I<time> an optional value used to control the printing of overdue messages. The
3012 default is now. It isn't the date of the invoice; that's the `_date' field.
3013 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3014 L<Time::Local> and L<Date::Parse> for conversion functions.
3016 I<template>, if specified, is the name of a suffix for alternate invoices.
3018 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3025 my ($file, $lfile) = $self->print_latex(@_);
3026 my $pdf = generate_pdf($file);
3032 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3034 Returns an HTML invoice, as a scalar.
3036 I<time> an optional value used to control the printing of overdue messages. The
3037 default is now. It isn't the date of the invoice; that's the `_date' field.
3038 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3039 L<Time::Local> and L<Date::Parse> for conversion functions.
3041 I<template>, if specified, is the name of a suffix for alternate invoices.
3043 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3045 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3046 when emailing the invoice as part of a multipart/related MIME email.
3054 %params = %{ shift() };
3056 $params{'time'} = shift;
3057 $params{'template'} = shift;
3058 $params{'cid'} = shift;
3061 $params{'format'} = 'html';
3063 $self->print_generic( %params );
3066 # quick subroutine for print_latex
3068 # There are ten characters that LaTeX treats as special characters, which
3069 # means that they do not simply typeset themselves:
3070 # # $ % & ~ _ ^ \ { }
3072 # TeX ignores blanks following an escaped character; if you want a blank (as
3073 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3077 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3078 $value =~ s/([<>])/\$$1\$/g;
3082 #utility methods for print_*
3084 sub _translate_old_latex_format {
3085 warn "_translate_old_latex_format called\n"
3092 if ( $line =~ /^%%Detail\s*$/ ) {
3094 push @template, q![@--!,
3095 q! foreach my $_tr_line (@detail_items) {!,
3096 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3097 q! $_tr_line->{'description'} .= !,
3098 q! "\\tabularnewline\n~~".!,
3099 q! join( "\\tabularnewline\n~~",!,
3100 q! @{$_tr_line->{'ext_description'}}!,
3104 while ( ( my $line_item_line = shift )
3105 !~ /^%%EndDetail\s*$/ ) {
3106 $line_item_line =~ s/'/\\'/g; # nice LTS
3107 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3108 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3109 push @template, " \$OUT .= '$line_item_line';";
3112 push @template, '}',
3115 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3117 push @template, '[@--',
3118 ' foreach my $_tr_line (@total_items) {';
3120 while ( ( my $total_item_line = shift )
3121 !~ /^%%EndTotalDetails\s*$/ ) {
3122 $total_item_line =~ s/'/\\'/g; # nice LTS
3123 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3124 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3125 push @template, " \$OUT .= '$total_item_line';";
3128 push @template, '}',
3132 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3133 push @template, $line;
3139 warn "$_\n" foreach @template;
3148 #check for an invoice-specific override
3149 return $self->invoice_terms if $self->invoice_terms;
3151 #check for a customer- specific override
3152 my $cust_main = $self->cust_main;
3153 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3155 #use configured default
3156 $conf->config('invoice_default_terms') || '';
3162 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3163 $duedate = $self->_date() + ( $1 * 86400 );
3170 $self->due_date ? time2str(shift, $self->due_date) : '';
3173 sub balance_due_msg {
3175 my $msg = 'Balance Due';
3176 return $msg unless $self->terms;
3177 if ( $self->due_date ) {
3178 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3179 } elsif ( $self->terms ) {
3180 $msg .= ' - '. $self->terms;
3185 sub balance_due_date {
3188 if ( $conf->exists('invoice_default_terms')
3189 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3190 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3195 =item invnum_date_pretty
3197 Returns a string with the invoice number and date, for example:
3198 "Invoice #54 (3/20/2008)"
3202 sub invnum_date_pretty {
3204 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3209 Returns a string with the date, for example: "3/20/2008"
3215 time2str($date_format, $self->_date);
3218 use vars qw(%pkg_category_cache);
3219 sub _items_sections {
3222 my $summarypage = shift;
3224 my $extra_sections = shift;
3228 my %late_subtotal = ();
3231 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3234 my $usage = $cust_bill_pkg->usage;
3236 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3237 next if ( $display->summary && $summarypage );
3239 my $section = $display->section;
3240 my $type = $display->type;
3242 $not_tax{$section} = 1
3243 unless $cust_bill_pkg->pkgnum == 0;
3245 if ( $display->post_total && !$summarypage ) {
3246 if (! $type || $type eq 'S') {
3247 $late_subtotal{$section} += $cust_bill_pkg->setup
3248 if $cust_bill_pkg->setup != 0;
3252 $late_subtotal{$section} += $cust_bill_pkg->recur
3253 if $cust_bill_pkg->recur != 0;
3256 if ($type && $type eq 'R') {
3257 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3258 if $cust_bill_pkg->recur != 0;
3261 if ($type && $type eq 'U') {
3262 $late_subtotal{$section} += $usage
3263 unless scalar(@$extra_sections);
3268 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3270 if (! $type || $type eq 'S') {
3271 $subtotal{$section} += $cust_bill_pkg->setup
3272 if $cust_bill_pkg->setup != 0;
3276 $subtotal{$section} += $cust_bill_pkg->recur
3277 if $cust_bill_pkg->recur != 0;
3280 if ($type && $type eq 'R') {
3281 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3282 if $cust_bill_pkg->recur != 0;
3285 if ($type && $type eq 'U') {
3286 $subtotal{$section} += $usage
3287 unless scalar(@$extra_sections);
3296 %pkg_category_cache = ();
3298 push @$late, map { { 'description' => &{$escape}($_),
3299 'subtotal' => $late_subtotal{$_},
3301 'sort_weight' => ( _pkg_category($_)
3302 ? _pkg_category($_)->weight
3305 ((_pkg_category($_) && _pkg_category($_)->condense)
3306 ? $self->_condense_section($format)
3310 sort _sectionsort keys %late_subtotal;
3313 if ( $summarypage ) {
3314 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3315 map { $_->categoryname } qsearch('pkg_category', {});
3317 @sections = keys %subtotal;
3320 my @early = map { { 'description' => &{$escape}($_),
3321 'subtotal' => $subtotal{$_},
3322 'summarized' => $not_tax{$_} ? '' : 'Y',
3323 'tax_section' => $not_tax{$_} ? '' : 'Y',
3324 'sort_weight' => ( _pkg_category($_)
3325 ? _pkg_category($_)->weight
3328 ((_pkg_category($_) && _pkg_category($_)->condense)
3329 ? $self->_condense_section($format)
3334 push @early, @$extra_sections if $extra_sections;
3336 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3340 #helper subs for above
3343 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3347 my $categoryname = shift;
3348 $pkg_category_cache{$categoryname} ||=
3349 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3352 my %condensed_format = (
3353 'label' => [ qw( Description Qty Amount ) ],
3355 sub { shift->{description} },
3356 sub { shift->{quantity} },
3357 sub { shift->{amount} },
3359 'align' => [ qw( l r r ) ],
3360 'span' => [ qw( 5 1 1 ) ], # unitprices?
3361 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3364 sub _condense_section {
3365 my ( $self, $format ) = ( shift, shift );
3367 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3368 qw( description_generator
3371 total_line_generator
3376 sub _condensed_generator_defaults {
3377 my ( $self, $format ) = ( shift, shift );
3378 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3387 sub _condensed_header_generator {
3388 my ( $self, $format ) = ( shift, shift );
3390 my ( $f, $prefix, $suffix, $separator, $column ) =
3391 _condensed_generator_defaults($format);
3393 if ($format eq 'latex') {
3394 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3395 $suffix = "\\\\\n\\hline";
3398 sub { my ($d,$a,$s,$w) = @_;
3399 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3401 } elsif ( $format eq 'html' ) {
3402 $prefix = '<th></th>';
3406 sub { my ($d,$a,$s,$w) = @_;
3407 return qq!<th align="$html_align{$a}">$d</th>!;
3415 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3417 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3420 $prefix. join($separator, @result). $suffix;
3425 sub _condensed_description_generator {
3426 my ( $self, $format ) = ( shift, shift );
3428 my ( $f, $prefix, $suffix, $separator, $column ) =
3429 _condensed_generator_defaults($format);
3431 if ($format eq 'latex') {
3432 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3434 $separator = " & \n";
3436 sub { my ($d,$a,$s,$w) = @_;
3437 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3439 }elsif ( $format eq 'html' ) {
3440 $prefix = '"><td align="center"></td>';
3444 sub { my ($d,$a,$s,$w) = @_;
3445 return qq!<td align="$html_align{$a}">$d</td>!;
3453 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3454 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3455 map { $f->{$_}->[$i] } qw(align span width)
3459 $prefix. join( $separator, @result ). $suffix;
3464 sub _condensed_total_generator {
3465 my ( $self, $format ) = ( shift, shift );
3467 my ( $f, $prefix, $suffix, $separator, $column ) =
3468 _condensed_generator_defaults($format);
3471 if ($format eq 'latex') {
3474 $separator = " & \n";
3476 sub { my ($d,$a,$s,$w) = @_;
3477 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3479 }elsif ( $format eq 'html' ) {
3483 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3485 sub { my ($d,$a,$s,$w) = @_;
3486 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3495 # my $r = &{$f->{fields}->[$i]}(@args);
3496 # $r .= ' Total' unless $i;
3498 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3500 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3501 map { $f->{$_}->[$i] } qw(align span width)
3505 $prefix. join( $separator, @result ). $suffix;
3510 =item total_line_generator FORMAT
3512 Returns a coderef used for generation of invoice total line items for this
3513 usage_class. FORMAT is either html or latex
3517 # should not be used: will have issues with hash element names (description vs
3518 # total_item and amount vs total_amount -- another array of functions?
3520 sub _condensed_total_line_generator {
3521 my ( $self, $format ) = ( shift, shift );
3523 my ( $f, $prefix, $suffix, $separator, $column ) =
3524 _condensed_generator_defaults($format);
3527 if ($format eq 'latex') {
3530 $separator = " & \n";
3532 sub { my ($d,$a,$s,$w) = @_;
3533 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3535 }elsif ( $format eq 'html' ) {
3539 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3541 sub { my ($d,$a,$s,$w) = @_;
3542 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3551 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3553 &{$column}( &{$f->{fields}->[$i]}(@args),
3554 map { $f->{$_}->[$i] } qw(align span width)
3558 $prefix. join( $separator, @result ). $suffix;
3563 #sub _items_extra_usage_sections {
3565 # my $escape = shift;
3567 # my %sections = ();
3569 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3570 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3572 # next unless $cust_bill_pkg->pkgnum > 0;
3574 # foreach my $section ( keys %usage_class ) {
3576 # my $usage = $cust_bill_pkg->usage($section);
3578 # next unless $usage && $usage > 0;
3580 # $sections{$section} ||= 0;
3581 # $sections{$section} += $usage;
3587 # map { { 'description' => &{$escape}($_),
3588 # 'subtotal' => $sections{$_},
3589 # 'summarized' => '',
3590 # 'tax_section' => '',
3593 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3597 sub _items_extra_usage_sections {
3606 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3607 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3608 next unless $cust_bill_pkg->pkgnum > 0;
3610 foreach my $classnum ( keys %usage_class ) {
3611 my $section = $usage_class{$classnum}->classname;
3612 $classnums{$section} = $classnum;
3614 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3615 my $amount = $detail->amount;
3616 next unless $amount && $amount > 0;
3618 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3619 $sections{$section}{amount} += $amount; #subtotal
3620 $sections{$section}{calls}++;
3621 $sections{$section}{duration} += $detail->duration;
3623 my $desc = $detail->regionname;
3624 my $description = $desc;
3625 $description = substr($desc, 0, 50). '...'
3626 if $format eq 'latex' && length($desc) > 50;
3628 $lines{$section}{$desc} ||= {
3629 description => &{$escape}($description),
3630 #pkgpart => $part_pkg->pkgpart,
3631 pkgnum => $cust_bill_pkg->pkgnum,
3636 #unit_amount => $cust_bill_pkg->unitrecur,
3637 quantity => $cust_bill_pkg->quantity,
3638 product_code => 'N/A',
3639 ext_description => [],
3642 $lines{$section}{$desc}{amount} += $amount;
3643 $lines{$section}{$desc}{calls}++;
3644 $lines{$section}{$desc}{duration} += $detail->duration;
3650 my %sectionmap = ();
3651 foreach (keys %sections) {
3652 my $usage_class = $usage_class{$classnums{$_}};
3653 $sectionmap{$_} = { 'description' => &{$escape}($_),
3654 'amount' => $sections{$_}{amount}, #subtotal
3655 'calls' => $sections{$_}{calls},
3656 'duration' => $sections{$_}{duration},
3658 'tax_section' => '',
3659 'sort_weight' => $usage_class->weight,
3660 ( $usage_class->format
3661 ? ( map { $_ => $usage_class->$_($format) }
3662 qw( description_generator header_generator total_generator total_line_generator )
3669 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3673 foreach my $section ( keys %lines ) {
3674 foreach my $line ( keys %{$lines{$section}} ) {
3675 my $l = $lines{$section}{$line};
3676 $l->{section} = $sectionmap{$section};
3677 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3678 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3683 return(\@sections, \@lines);
3687 sub _items_svc_phone_sections {
3696 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3698 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3699 next unless $cust_bill_pkg->pkgnum > 0;
3701 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3703 my $phonenum = $detail->phonenum;
3704 next unless $phonenum;
3706 my $amount = $detail->amount;
3707 next unless $amount && $amount > 0;
3709 $sections{$phonenum} ||= { 'amount' => 0,
3712 'sort_weight' => -1,
3713 'phonenum' => $phonenum,
3715 $sections{$phonenum}{amount} += $amount; #subtotal
3716 $sections{$phonenum}{calls}++;
3717 $sections{$phonenum}{duration} += $detail->duration;
3719 my $desc = $detail->regionname;
3720 my $description = $desc;
3721 $description = substr($desc, 0, 50). '...'
3722 if $format eq 'latex' && length($desc) > 50;
3724 $lines{$phonenum}{$desc} ||= {
3725 description => &{$escape}($description),
3726 #pkgpart => $part_pkg->pkgpart,
3734 product_code => 'N/A',
3735 ext_description => [],
3738 $lines{$phonenum}{$desc}{amount} += $amount;
3739 $lines{$phonenum}{$desc}{calls}++;
3740 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3742 my $line = $usage_class{$detail->classnum}->classname;
3743 $sections{"$phonenum $line"} ||=
3747 'sort_weight' => $usage_class{$detail->classnum}->weight,
3748 'phonenum' => $phonenum,
3750 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3751 $sections{"$phonenum $line"}{calls}++;
3752 $sections{"$phonenum $line"}{duration} += $detail->duration;
3754 $lines{"$phonenum $line"}{$desc} ||= {
3755 description => &{$escape}($description),
3756 #pkgpart => $part_pkg->pkgpart,
3764 product_code => 'N/A',
3765 ext_description => [],
3768 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3769 $lines{"$phonenum $line"}{$desc}{calls}++;
3770 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3771 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3772 $detail->formatted('format' => $format);
3777 my %sectionmap = ();
3778 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3779 my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3780 foreach ( keys %sections ) {
3781 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3782 my $usage_class = $summary ? $simple : $usage_simple;
3783 my $ending = $summary ? ' usage charges' : '';
3784 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3785 'amount' => $sections{$_}{amount}, #subtotal
3786 'calls' => $sections{$_}{calls},
3787 'duration' => $sections{$_}{duration},
3789 'tax_section' => '',
3790 'phonenum' => $sections{$_}{phonenum},
3791 'sort_weight' => $sections{$_}{sort_weight},
3792 'post_total' => $summary, #inspire pagebreak
3794 ( map { $_ => $usage_class->$_($format) }
3795 qw( description_generator
3798 total_line_generator
3805 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3806 $a->{sort_weight} <=> $b->{sort_weight}
3811 foreach my $section ( keys %lines ) {
3812 foreach my $line ( keys %{$lines{$section}} ) {
3813 my $l = $lines{$section}{$line};
3814 $l->{section} = $sectionmap{$section};
3815 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3816 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3821 return(\@sections, \@lines);
3828 #my @display = scalar(@_)
3830 # : qw( _items_previous _items_pkg );
3831 # #: qw( _items_pkg );
3832 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3833 my @display = qw( _items_previous _items_pkg );
3836 foreach my $display ( @display ) {
3837 push @b, $self->$display(@_);
3842 sub _items_previous {
3844 my $cust_main = $self->cust_main;
3845 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3847 foreach ( @pr_cust_bill ) {
3848 my $date = $conf->exists('invoice_show_prior_due_date')
3849 ? 'due '. $_->due_date2str($date_format)
3850 : time2str($date_format, $_->_date);
3852 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3853 #'pkgpart' => 'N/A',
3855 'amount' => sprintf("%.2f", $_->owed),
3861 # 'description' => 'Previous Balance',
3862 # #'pkgpart' => 'N/A',
3863 # 'pkgnum' => 'N/A',
3864 # 'amount' => sprintf("%10.2f", $pr_total ),
3865 # 'ext_description' => [ map {
3866 # "Invoice ". $_->invnum.
3867 # " (". time2str("%x",$_->_date). ") ".
3868 # sprintf("%10.2f", $_->owed)
3869 # } @pr_cust_bill ],
3877 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3878 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3879 if ($options{section} && $options{section}->{condensed}) {
3881 local $Storable::canonical = 1;
3882 foreach ( @items ) {
3884 delete $item->{ref};
3885 delete $item->{ext_description};
3886 my $key = freeze($item);
3887 $itemshash{$key} ||= 0;
3888 $itemshash{$key} ++; # += $item->{quantity};
3890 @items = sort { $a->{description} cmp $b->{description} }
3891 map { my $i = thaw($_);
3892 $i->{quantity} = $itemshash{$_};
3894 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3903 return 0 unless $a cmp $b;
3904 return -1 if $b eq 'Tax';
3905 return 1 if $a eq 'Tax';
3906 return -1 if $b eq 'Other surcharges';
3907 return 1 if $a eq 'Other surcharges';
3913 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3914 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3917 sub _items_cust_bill_pkg {
3919 my $cust_bill_pkg = shift;
3922 my $format = $opt{format} || '';
3923 my $escape_function = $opt{escape_function} || sub { shift };
3924 my $format_function = $opt{format_function} || '';
3925 my $unsquelched = $opt{unsquelched} || '';
3926 my $section = $opt{section}->{description} if $opt{section};
3927 my $summary_page = $opt{summary_page} || '';
3928 my $multilocation = $opt{multilocation} || '';
3931 my ($s, $r, $u) = ( undef, undef, undef );
3932 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3935 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3936 if ( $_ && !$cust_bill_pkg->hidden ) {
3937 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3938 $_->{amount} =~ s/^\-0\.00$/0.00/;
3939 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3941 unless $_->{amount} == 0;
3946 foreach my $display ( grep { defined($section)
3947 ? $_->section eq $section
3950 grep { !$_->summary || !$summary_page }
3951 $cust_bill_pkg->cust_bill_pkg_display
3955 my $type = $display->type;
3957 my $desc = $cust_bill_pkg->desc;
3958 $desc = substr($desc, 0, 50). '...'
3959 if $format eq 'latex' && length($desc) > 50;
3961 my %details_opt = ( 'format' => $format,
3962 'escape_function' => $escape_function,
3963 'format_function' => $format_function,
3966 if ( $cust_bill_pkg->pkgnum > 0 ) {
3968 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3970 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3972 my $description = $desc;
3973 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3976 unless ( $cust_pkg->part_pkg->hide_svc_detail
3977 || $cust_bill_pkg->hidden )
3979 push @d, map &{$escape_function}($_),
3980 $cust_pkg->h_labels_short($self->_date);
3981 if ( $multilocation ) {
3982 my $loc = $cust_pkg->location_label;
3983 $loc = substr($desc, 0, 50). '...'
3984 if $format eq 'latex' && length($loc) > 50;
3985 push @d, &{$escape_function}($loc);
3988 push @d, $cust_bill_pkg->details(%details_opt)
3989 if $cust_bill_pkg->recur == 0;
3991 if ( $cust_bill_pkg->hidden ) {
3992 $s->{amount} += $cust_bill_pkg->setup;
3993 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3994 push @{ $s->{ext_description} }, @d;
3997 description => $description,
3998 #pkgpart => $part_pkg->pkgpart,
3999 pkgnum => $cust_bill_pkg->pkgnum,
4000 amount => $cust_bill_pkg->setup,
4001 unit_amount => $cust_bill_pkg->unitsetup,
4002 quantity => $cust_bill_pkg->quantity,
4003 ext_description => \@d,
4009 if ( $cust_bill_pkg->recur != 0 &&
4010 ( !$type || $type eq 'R' || $type eq 'U' )
4014 my $is_summary = $display->summary;
4015 my $description = ($is_summary && $type && $type eq 'U')
4016 ? "Usage charges" : $desc;
4018 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4019 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4020 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4025 #at least until cust_bill_pkg has "past" ranges in addition to
4026 #the "future" sdate/edate ones... see #3032
4027 my @dates = ( $self->_date );
4028 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4029 push @dates, $prev->sdate if $prev;
4031 unless ( $cust_pkg->part_pkg->hide_svc_detail
4032 || $cust_bill_pkg->itemdesc
4033 || $cust_bill_pkg->hidden
4034 || $is_summary && $type && $type eq 'U' )
4036 push @d, map &{$escape_function}($_),
4037 $cust_pkg->h_labels_short(@dates)
4038 #$cust_bill_pkg->edate,
4039 #$cust_bill_pkg->sdate)
4041 if ( $multilocation ) {
4042 my $loc = $cust_pkg->location_label;
4043 $loc = substr($desc, 0, 50). '...'
4044 if $format eq 'latex' && length($loc) > 50;
4045 push @d, &{$escape_function}($loc);
4049 push @d, $cust_bill_pkg->details(%details_opt)
4050 unless ($is_summary || $type && $type eq 'R');
4054 $amount = $cust_bill_pkg->recur;
4055 }elsif($type eq 'R') {
4056 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4057 }elsif($type eq 'U') {
4058 $amount = $cust_bill_pkg->usage;
4061 if ( !$type || $type eq 'R' ) {
4063 if ( $cust_bill_pkg->hidden ) {
4064 $r->{amount} += $amount;
4065 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4066 push @{ $r->{ext_description} }, @d;
4069 description => $description,
4070 #pkgpart => $part_pkg->pkgpart,
4071 pkgnum => $cust_bill_pkg->pkgnum,
4073 unit_amount => $cust_bill_pkg->unitrecur,
4074 quantity => $cust_bill_pkg->quantity,
4075 ext_description => \@d,
4079 } elsif ( $amount ) { # && $type eq 'U'
4081 if ( $cust_bill_pkg->hidden ) {
4082 $u->{amount} += $amount;
4083 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4084 push @{ $u->{ext_description} }, @d;
4087 description => $description,
4088 #pkgpart => $part_pkg->pkgpart,
4089 pkgnum => $cust_bill_pkg->pkgnum,
4091 unit_amount => $cust_bill_pkg->unitrecur,
4092 quantity => $cust_bill_pkg->quantity,
4093 ext_description => \@d,
4099 } # recurring or usage with recurring charge
4101 } else { #pkgnum tax or one-shot line item (??)
4103 if ( $cust_bill_pkg->setup != 0 ) {
4105 'description' => $desc,
4106 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4109 if ( $cust_bill_pkg->recur != 0 ) {
4111 'description' => "$desc (".
4112 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4113 time2str($date_format, $cust_bill_pkg->edate). ')',
4114 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4124 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4126 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4127 $_->{amount} =~ s/^\-0\.00$/0.00/;
4128 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4130 unless $_->{amount} == 0;
4138 sub _items_credits {
4139 my( $self, %opt ) = @_;
4140 my $trim_len = $opt{'trim_len'} || 60;
4144 foreach ( $self->cust_credited ) {
4146 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4148 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4149 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4150 $reason = " ($reason) " if $reason;
4153 #'description' => 'Credit ref\#'. $_->crednum.
4154 # " (". time2str("%x",$_->cust_credit->_date) .")".
4156 'description' => 'Credit applied '.
4157 time2str($date_format,$_->cust_credit->_date). $reason,
4158 'amount' => sprintf("%.2f",$_->amount),
4166 sub _items_payments {
4170 #get & print payments
4171 foreach ( $self->cust_bill_pay ) {
4173 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4176 'description' => "Payment received ".
4177 time2str($date_format,$_->cust_pay->_date ),
4178 'amount' => sprintf("%.2f", $_->amount )
4186 =item call_details [ OPTION => VALUE ... ]
4188 Returns an array of CSV strings representing the call details for this invoice
4189 The only option available is the boolean prepend_billed_number
4194 my ($self, %opt) = @_;
4196 my $format_function = sub { shift };
4198 if ($opt{prepend_billed_number}) {
4199 $format_function = sub {
4203 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4208 my @details = map { $_->details( 'format_function' => $format_function,
4209 'escape_function' => sub{ return() },
4213 $self->cust_bill_pkg;
4214 my $header = $details[0];
4215 ( $header, grep { $_ ne $header } @details );
4225 =item process_reprint
4229 sub process_reprint {
4230 process_re_X('print', @_);
4233 =item process_reemail
4237 sub process_reemail {
4238 process_re_X('email', @_);
4246 process_re_X('fax', @_);
4254 process_re_X('ftp', @_);
4261 sub process_respool {
4262 process_re_X('spool', @_);
4265 use Storable qw(thaw);
4269 my( $method, $job ) = ( shift, shift );
4270 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4272 my $param = thaw(decode_base64(shift));
4273 warn Dumper($param) if $DEBUG;
4284 my($method, $job, %param ) = @_;
4286 warn "re_X $method for job $job with param:\n".
4287 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4290 #some false laziness w/search/cust_bill.html
4292 my $orderby = 'ORDER BY cust_bill._date';
4294 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4296 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4298 my @cust_bill = qsearch( {
4299 #'select' => "cust_bill.*",
4300 'table' => 'cust_bill',
4301 'addl_from' => $addl_from,
4303 'extra_sql' => $extra_sql,
4304 'order_by' => $orderby,
4308 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4310 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4313 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4314 foreach my $cust_bill ( @cust_bill ) {
4315 $cust_bill->$method();
4317 if ( $job ) { #progressbar foo
4319 if ( time - $min_sec > $last ) {
4320 my $error = $job->update_statustext(
4321 int( 100 * $num / scalar(@cust_bill) )
4323 die $error if $error;
4334 =head1 CLASS METHODS
4340 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4346 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4351 Returns an SQL fragment to retreive the net amount (charged minus credited).
4357 'charged - '. $class->credited_sql;
4362 Returns an SQL fragment to retreive the amount paid against this invoice.
4368 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4369 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
4374 Returns an SQL fragment to retreive the amount credited against this invoice.
4380 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4381 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
4384 =item search_sql_where HASHREF
4386 Class method which returns an SQL WHERE fragment to search for parameters
4387 specified in HASHREF. Valid parameters are
4393 List reference of start date, end date, as UNIX timestamps.
4403 List reference of charged limits (exclusive).
4407 List reference of charged limits (exclusive).
4411 flag, return open invoices only
4415 flag, return net invoices only
4419 =item newest_percust
4423 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4427 sub search_sql_where {
4428 my($class, $param) = @_;
4430 warn "$me search_sql_where called with params: \n".
4431 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4437 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4438 push @search, "cust_main.agentnum = $1";
4442 if ( $param->{_date} ) {
4443 my($beginning, $ending) = @{$param->{_date}};
4445 push @search, "cust_bill._date >= $beginning",
4446 "cust_bill._date < $ending";
4450 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4451 push @search, "cust_bill.invnum >= $1";
4453 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4454 push @search, "cust_bill.invnum <= $1";
4458 if ( $param->{charged} ) {
4459 my @charged = ref($param->{charged})
4460 ? @{ $param->{charged} }
4461 : ($param->{charged});
4463 push @search, map { s/^charged/cust_bill.charged/; $_; }
4467 my $owed_sql = FS::cust_bill->owed_sql;
4470 if ( $param->{owed} ) {
4471 my @owed = ref($param->{owed})
4472 ? @{ $param->{owed} }
4474 push @search, map { s/^owed/$owed_sql/; $_; }
4479 push @search, "0 != $owed_sql"
4480 if $param->{'open'};
4481 push @search, '0 != '. FS::cust_bill->net_sql
4485 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4486 if $param->{'days'};
4489 if ( $param->{'newest_percust'} ) {
4491 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4492 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4494 my @newest_where = map { my $x = $_;
4495 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4498 grep ! /^cust_main./, @search;
4499 my $newest_where = scalar(@newest_where)
4500 ? ' AND '. join(' AND ', @newest_where)
4504 push @search, "cust_bill._date = (
4505 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4506 WHERE newest_cust_bill.custnum = cust_bill.custnum
4512 #agent virtualization
4513 my $curuser = $FS::CurrentUser::CurrentUser;
4514 if ( $curuser->username eq 'fs_queue'
4515 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4517 my $newuser = qsearchs('access_user', {
4518 'username' => $username,
4522 $curuser = $newuser;
4524 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4527 push @search, $curuser->agentnums_sql;
4529 join(' AND ', @search );
4541 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4542 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base