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'),
2332 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2334 # better hang on to conf_dir for a while (for old templates)
2335 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2337 #these are only used when doing paged plaintext
2343 $invoice_data{finance_section} = '';
2344 if ( $conf->config('finance_pkgclass') ) {
2346 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2347 $invoice_data{finance_section} = $pkg_class->categoryname;
2349 $invoice_data{finance_amount} = '0.00';
2351 my $countrydefault = $conf->config('countrydefault') || 'US';
2352 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2353 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2354 my $method = $prefix.$_;
2355 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2357 $invoice_data{'ship_country'} = ''
2358 if ( $invoice_data{'ship_country'} eq $countrydefault );
2360 $invoice_data{'cid'} = $params{'cid'}
2363 if ( $cust_main->country eq $countrydefault ) {
2364 $invoice_data{'country'} = '';
2366 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2370 $invoice_data{'address'} = \@address;
2372 $cust_main->payname.
2373 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2374 ? " (P.O. #". $cust_main->payinfo. ")"
2378 push @address, $cust_main->company
2379 if $cust_main->company;
2380 push @address, $cust_main->address1;
2381 push @address, $cust_main->address2
2382 if $cust_main->address2;
2384 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2385 push @address, $invoice_data{'country'}
2386 if $invoice_data{'country'};
2388 while (scalar(@address) < 5);
2390 $invoice_data{'logo_file'} = $params{'logo_file'}
2391 if $params{'logo_file'};
2393 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2394 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2395 #my $balance_due = $self->owed + $pr_total - $cr_total;
2396 my $balance_due = $self->owed + $pr_total;
2397 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2398 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2399 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2400 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2402 my $agentnum = $self->cust_main->agentnum;
2404 my $summarypage = '';
2405 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2408 $invoice_data{'summarypage'} = $summarypage;
2410 #do variable substitution in notes, footer, smallfooter
2411 foreach my $include (qw( notes footer smallfooter coupon )) {
2413 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2416 if ( $conf->exists($inc_file, $agentnum)
2417 && length( $conf->config($inc_file, $agentnum) ) ) {
2419 @inc_src = $conf->config($inc_file, $agentnum);
2423 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2425 my $convert_map = $convert_maps{$format}{$include};
2427 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2428 s/--\@\]/$delimiters{$format}[1]/g;
2431 &$convert_map( $conf->config($inc_file, $agentnum) );
2435 my $inc_tt = new Text::Template (
2437 SOURCE => [ map "$_\n", @inc_src ],
2438 DELIMITERS => $delimiters{$format},
2439 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2441 unless ( $inc_tt->compile() ) {
2442 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2443 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2447 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2449 $invoice_data{$include} =~ s/\n+$//
2450 if ($format eq 'latex');
2453 $invoice_data{'po_line'} =
2454 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2455 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2458 my %money_chars = ( 'latex' => '',
2459 'html' => $conf->config('money_char') || '$',
2462 my $money_char = $money_chars{$format};
2464 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2465 'html' => $conf->config('money_char') || '$',
2468 my $other_money_char = $other_money_chars{$format};
2469 $invoice_data{'dollar'} = $other_money_char;
2471 my @detail_items = ();
2472 my @total_items = ();
2476 $invoice_data{'detail_items'} = \@detail_items;
2477 $invoice_data{'total_items'} = \@total_items;
2478 $invoice_data{'buf'} = \@buf;
2479 $invoice_data{'sections'} = \@sections;
2481 my $previous_section = { 'description' => 'Previous Charges',
2482 'subtotal' => $other_money_char.
2483 sprintf('%.2f', $pr_total),
2484 'summarized' => $summarypage ? 'Y' : '',
2486 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2487 join(' / ', map { $cust_main->balance_date_range(@$_) }
2488 $self->_prior_month30s
2490 if $conf->exists('invoice_include_aging');
2493 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2494 'subtotal' => $taxtotal, # adjusted below
2495 'summarized' => $summarypage ? 'Y' : '',
2497 my $tax_weight = _pkg_category($tax_section->{description})
2498 ? _pkg_category($tax_section->{description})->weight
2500 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2501 $tax_section->{'sort_weight'} = $tax_weight;
2504 my $adjusttotal = 0;
2505 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2506 'subtotal' => 0, # adjusted below
2507 'summarized' => $summarypage ? 'Y' : '',
2509 my $adjust_weight = _pkg_category($adjust_section->{description})
2510 ? _pkg_category($adjust_section->{description})->weight
2512 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2513 $adjust_section->{'sort_weight'} = $adjust_weight;
2515 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2516 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2517 $invoice_data{'multisection'} = $multisection;
2518 my $late_sections = [];
2519 my $extra_sections = [];
2520 my $extra_lines = ();
2521 if ( $multisection ) {
2522 ($extra_sections, $extra_lines) =
2523 $self->_items_extra_usage_sections($escape_function, $format)
2524 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2526 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2528 push @detail_items, @$extra_lines if $extra_lines;
2530 $self->_items_sections( $late_sections, # this could stand a refactor
2536 if ($conf->exists('svc_phone_sections')) {
2537 my ($phone_sections, $phone_lines) =
2538 $self->_items_svc_phone_sections($escape_function, $format);
2539 push @{$late_sections}, @$phone_sections;
2540 push @detail_items, @$phone_lines;
2543 push @sections, { 'description' => '', 'subtotal' => '' };
2546 unless ( $conf->exists('disable_previous_balance')
2547 || $conf->exists('previous_balance-summary_only')
2551 foreach my $line_item ( $self->_items_previous ) {
2554 ext_description => [],
2556 $detail->{'ref'} = $line_item->{'pkgnum'};
2557 $detail->{'quantity'} = 1;
2558 $detail->{'section'} = $previous_section;
2559 $detail->{'description'} = &$escape_function($line_item->{'description'});
2560 if ( exists $line_item->{'ext_description'} ) {
2561 @{$detail->{'ext_description'}} = map {
2562 &$escape_function($_);
2563 } @{$line_item->{'ext_description'}};
2565 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2566 $line_item->{'amount'};
2567 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2569 push @detail_items, $detail;
2570 push @buf, [ $detail->{'description'},
2571 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2577 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2578 push @buf, ['','-----------'];
2579 push @buf, [ 'Total Previous Balance',
2580 $money_char. sprintf("%10.2f", $pr_total) ];
2584 foreach my $section (@sections, @$late_sections) {
2586 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2587 if ( $invoice_data{finance_section} &&
2588 $section->{'description'} eq $invoice_data{finance_section} );
2590 $section->{'subtotal'} = $other_money_char.
2591 sprintf('%.2f', $section->{'subtotal'})
2594 # begin some normalization
2595 $section->{'amount'} = $section->{'subtotal'}
2599 if ( $section->{'description'} ) {
2600 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2605 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2607 $options{'section'} = $section if $multisection;
2608 $options{'format'} = $format;
2609 $options{'escape_function'} = $escape_function;
2610 $options{'format_function'} = sub { () } unless $unsquelched;
2611 $options{'unsquelched'} = $unsquelched;
2612 $options{'summary_page'} = $summarypage;
2613 $options{'skip_usage'} =
2614 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2615 $options{'multilocation'} = $multilocation;
2617 foreach my $line_item ( $self->_items_pkg(%options) ) {
2619 ext_description => [],
2621 $detail->{'ref'} = $line_item->{'pkgnum'};
2622 $detail->{'quantity'} = $line_item->{'quantity'};
2623 $detail->{'section'} = $section;
2624 $detail->{'description'} = &$escape_function($line_item->{'description'});
2625 if ( exists $line_item->{'ext_description'} ) {
2626 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2628 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2629 $line_item->{'amount'};
2630 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2631 $line_item->{'unit_amount'};
2632 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2634 push @detail_items, $detail;
2635 push @buf, ( [ $detail->{'description'},
2636 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2638 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2642 if ( $section->{'description'} ) {
2643 push @buf, ( ['','-----------'],
2644 [ $section->{'description'}. ' sub-total',
2645 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2654 $invoice_data{current_less_finance} =
2655 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2657 if ( $multisection && !$conf->exists('disable_previous_balance')
2658 || $conf->exists('previous_balance-summary_only') )
2660 unshift @sections, $previous_section if $pr_total;
2663 foreach my $tax ( $self->_items_tax ) {
2665 $taxtotal += $tax->{'amount'};
2667 my $description = &$escape_function( $tax->{'description'} );
2668 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2670 if ( $multisection ) {
2672 my $money = $old_latex ? '' : $money_char;
2673 push @detail_items, {
2674 ext_description => [],
2677 description => $description,
2678 amount => $money. $amount,
2680 section => $tax_section,
2685 push @total_items, {
2686 'total_item' => $description,
2687 'total_amount' => $other_money_char. $amount,
2692 push @buf,[ $description,
2693 $money_char. $amount,
2700 $total->{'total_item'} = 'Sub-total';
2701 $total->{'total_amount'} =
2702 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2704 if ( $multisection ) {
2705 $tax_section->{'subtotal'} = $other_money_char.
2706 sprintf('%.2f', $taxtotal);
2707 $tax_section->{'pretotal'} = 'New charges sub-total '.
2708 $total->{'total_amount'};
2709 push @sections, $tax_section if $taxtotal;
2711 unshift @total_items, $total;
2714 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2716 push @buf,['','-----------'];
2717 push @buf,[( $conf->exists('disable_previous_balance')
2719 : 'Total New Charges'
2721 $money_char. sprintf("%10.2f",$self->charged) ];
2727 $item = $conf->config('previous_balance-exclude_from_total')
2728 || 'Total New Charges'
2729 if $conf->exists('previous_balance-exclude_from_total');
2730 my $amount = $self->charged +
2731 ( $conf->exists('disable_previous_balance') ||
2732 $conf->exists('previous_balance-exclude_from_total')
2736 $total->{'total_item'} = &$embolden_function($item);
2737 $total->{'total_amount'} =
2738 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2739 if ( $multisection ) {
2740 if ( $adjust_section->{'sort_weight'} ) {
2741 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2742 sprintf("%.2f", ($self->billing_balance || 0) );
2744 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2745 sprintf('%.2f', $self->charged );
2748 push @total_items, $total;
2750 push @buf,['','-----------'];
2753 sprintf( '%10.2f', $amount )
2758 unless ( $conf->exists('disable_previous_balance') ) {
2759 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2762 my $credittotal = 0;
2763 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2766 $total->{'total_item'} = &$escape_function($credit->{'description'});
2767 $credittotal += $credit->{'amount'};
2768 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2769 $adjusttotal += $credit->{'amount'};
2770 if ( $multisection ) {
2771 my $money = $old_latex ? '' : $money_char;
2772 push @detail_items, {
2773 ext_description => [],
2776 description => &$escape_function($credit->{'description'}),
2777 amount => $money. $credit->{'amount'},
2779 section => $adjust_section,
2782 push @total_items, $total;
2786 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2789 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2790 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2794 my $paymenttotal = 0;
2795 foreach my $payment ( $self->_items_payments ) {
2797 $total->{'total_item'} = &$escape_function($payment->{'description'});
2798 $paymenttotal += $payment->{'amount'};
2799 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2800 $adjusttotal += $payment->{'amount'};
2801 if ( $multisection ) {
2802 my $money = $old_latex ? '' : $money_char;
2803 push @detail_items, {
2804 ext_description => [],
2807 description => &$escape_function($payment->{'description'}),
2808 amount => $money. $payment->{'amount'},
2810 section => $adjust_section,
2813 push @total_items, $total;
2815 push @buf, [ $payment->{'description'},
2816 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2819 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2821 if ( $multisection ) {
2822 $adjust_section->{'subtotal'} = $other_money_char.
2823 sprintf('%.2f', $adjusttotal);
2824 push @sections, $adjust_section
2825 unless $adjust_section->{sort_weight};
2830 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2831 $total->{'total_amount'} =
2832 &$embolden_function(
2833 $other_money_char. sprintf('%.2f', $summarypage
2835 $self->billing_balance
2836 : $self->owed + $pr_total
2839 if ( $multisection && !$adjust_section->{sort_weight} ) {
2840 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2841 $total->{'total_amount'};
2843 push @total_items, $total;
2845 push @buf,['','-----------'];
2846 push @buf,[$self->balance_due_msg, $money_char.
2847 sprintf("%10.2f", $balance_due ) ];
2851 if ( $multisection ) {
2852 if ($conf->exists('svc_phone_sections')) {
2854 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2855 $total->{'total_amount'} =
2856 &$embolden_function(
2857 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2859 my $last_section = pop @sections;
2860 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2861 $total->{'total_amount'};
2862 push @sections, $last_section;
2864 push @sections, @$late_sections
2868 my @includelist = ();
2869 push @includelist, 'summary' if $summarypage;
2870 foreach my $include ( @includelist ) {
2872 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2875 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2877 @inc_src = $conf->config($inc_file, $agentnum);
2881 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2883 my $convert_map = $convert_maps{$format}{$include};
2885 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2886 s/--\@\]/$delimiters{$format}[1]/g;
2889 &$convert_map( $conf->config($inc_file, $agentnum) );
2893 my $inc_tt = new Text::Template (
2895 SOURCE => [ map "$_\n", @inc_src ],
2896 DELIMITERS => $delimiters{$format},
2897 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2899 unless ( $inc_tt->compile() ) {
2900 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2901 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2905 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2907 $invoice_data{$include} =~ s/\n+$//
2908 if ($format eq 'latex');
2913 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2914 /invoice_lines\((\d*)\)/;
2915 $invoice_lines += $1 || scalar(@buf);
2918 die "no invoice_lines() functions in template?"
2919 if ( $format eq 'template' && !$wasfunc );
2921 if ($format eq 'template') {
2923 if ( $invoice_lines ) {
2924 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2925 $invoice_data{'total_pages'}++
2926 if scalar(@buf) % $invoice_lines;
2929 #setup subroutine for the template
2930 sub FS::cust_bill::_template::invoice_lines {
2931 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2933 scalar(@FS::cust_bill::_template::buf)
2934 ? shift @FS::cust_bill::_template::buf
2943 push @collect, split("\n",
2944 $text_template->fill_in( HASH => \%invoice_data,
2945 PACKAGE => 'FS::cust_bill::_template'
2948 $FS::cust_bill::_template::page++;
2950 map "$_\n", @collect;
2952 warn "filling in template for invoice ". $self->invnum. "\n"
2954 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2957 $text_template->fill_in(HASH => \%invoice_data);
2961 # helper routine for generating date ranges
2962 sub _prior_month30s {
2965 [ 1, 2592000 ], # 0-30 days ago
2966 [ 2592000, 5184000 ], # 30-60 days ago
2967 [ 5184000, 7776000 ], # 60-90 days ago
2968 [ 7776000, 0 ], # 90+ days ago
2971 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2972 $_->[1] ? $self->_date - $_->[1] - 1 : '',
2977 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2979 Returns an postscript invoice, as a scalar.
2981 Options can be passed as a hashref (recommended) or as a list of time, template
2982 and then any key/value pairs for any other options.
2984 I<time> an optional value used to control the printing of overdue messages. The
2985 default is now. It isn't the date of the invoice; that's the `_date' field.
2986 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2987 L<Time::Local> and L<Date::Parse> for conversion functions.
2989 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2996 my ($file, $lfile) = $self->print_latex(@_);
2997 my $ps = generate_ps($file);
3003 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3005 Returns an PDF invoice, as a scalar.
3007 Options can be passed as a hashref (recommended) or as a list of time, template
3008 and then any key/value pairs for any other options.
3010 I<time> an optional value used to control the printing of overdue messages. The
3011 default is now. It isn't the date of the invoice; that's the `_date' field.
3012 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3013 L<Time::Local> and L<Date::Parse> for conversion functions.
3015 I<template>, if specified, is the name of a suffix for alternate invoices.
3017 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3024 my ($file, $lfile) = $self->print_latex(@_);
3025 my $pdf = generate_pdf($file);
3031 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3033 Returns an HTML invoice, as a scalar.
3035 I<time> an optional value used to control the printing of overdue messages. The
3036 default is now. It isn't the date of the invoice; that's the `_date' field.
3037 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3038 L<Time::Local> and L<Date::Parse> for conversion functions.
3040 I<template>, if specified, is the name of a suffix for alternate invoices.
3042 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3044 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3045 when emailing the invoice as part of a multipart/related MIME email.
3053 %params = %{ shift() };
3055 $params{'time'} = shift;
3056 $params{'template'} = shift;
3057 $params{'cid'} = shift;
3060 $params{'format'} = 'html';
3062 $self->print_generic( %params );
3065 # quick subroutine for print_latex
3067 # There are ten characters that LaTeX treats as special characters, which
3068 # means that they do not simply typeset themselves:
3069 # # $ % & ~ _ ^ \ { }
3071 # TeX ignores blanks following an escaped character; if you want a blank (as
3072 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3076 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3077 $value =~ s/([<>])/\$$1\$/g;
3081 #utility methods for print_*
3083 sub _translate_old_latex_format {
3084 warn "_translate_old_latex_format called\n"
3091 if ( $line =~ /^%%Detail\s*$/ ) {
3093 push @template, q![@--!,
3094 q! foreach my $_tr_line (@detail_items) {!,
3095 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3096 q! $_tr_line->{'description'} .= !,
3097 q! "\\tabularnewline\n~~".!,
3098 q! join( "\\tabularnewline\n~~",!,
3099 q! @{$_tr_line->{'ext_description'}}!,
3103 while ( ( my $line_item_line = shift )
3104 !~ /^%%EndDetail\s*$/ ) {
3105 $line_item_line =~ s/'/\\'/g; # nice LTS
3106 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3107 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3108 push @template, " \$OUT .= '$line_item_line';";
3111 push @template, '}',
3114 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3116 push @template, '[@--',
3117 ' foreach my $_tr_line (@total_items) {';
3119 while ( ( my $total_item_line = shift )
3120 !~ /^%%EndTotalDetails\s*$/ ) {
3121 $total_item_line =~ s/'/\\'/g; # nice LTS
3122 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3123 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3124 push @template, " \$OUT .= '$total_item_line';";
3127 push @template, '}',
3131 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3132 push @template, $line;
3138 warn "$_\n" foreach @template;
3147 #check for an invoice-specific override
3148 return $self->invoice_terms if $self->invoice_terms;
3150 #check for a customer- specific override
3151 my $cust_main = $self->cust_main;
3152 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3154 #use configured default
3155 $conf->config('invoice_default_terms') || '';
3161 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3162 $duedate = $self->_date() + ( $1 * 86400 );
3169 $self->due_date ? time2str(shift, $self->due_date) : '';
3172 sub balance_due_msg {
3174 my $msg = 'Balance Due';
3175 return $msg unless $self->terms;
3176 if ( $self->due_date ) {
3177 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3178 } elsif ( $self->terms ) {
3179 $msg .= ' - '. $self->terms;
3184 sub balance_due_date {
3187 if ( $conf->exists('invoice_default_terms')
3188 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3189 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3194 =item invnum_date_pretty
3196 Returns a string with the invoice number and date, for example:
3197 "Invoice #54 (3/20/2008)"
3201 sub invnum_date_pretty {
3203 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3208 Returns a string with the date, for example: "3/20/2008"
3214 time2str($date_format, $self->_date);
3217 use vars qw(%pkg_category_cache);
3218 sub _items_sections {
3221 my $summarypage = shift;
3223 my $extra_sections = shift;
3227 my %late_subtotal = ();
3230 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3233 my $usage = $cust_bill_pkg->usage;
3235 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3236 next if ( $display->summary && $summarypage );
3238 my $section = $display->section;
3239 my $type = $display->type;
3241 $not_tax{$section} = 1
3242 unless $cust_bill_pkg->pkgnum == 0;
3244 if ( $display->post_total && !$summarypage ) {
3245 if (! $type || $type eq 'S') {
3246 $late_subtotal{$section} += $cust_bill_pkg->setup
3247 if $cust_bill_pkg->setup != 0;
3251 $late_subtotal{$section} += $cust_bill_pkg->recur
3252 if $cust_bill_pkg->recur != 0;
3255 if ($type && $type eq 'R') {
3256 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3257 if $cust_bill_pkg->recur != 0;
3260 if ($type && $type eq 'U') {
3261 $late_subtotal{$section} += $usage
3262 unless scalar(@$extra_sections);
3267 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3269 if (! $type || $type eq 'S') {
3270 $subtotal{$section} += $cust_bill_pkg->setup
3271 if $cust_bill_pkg->setup != 0;
3275 $subtotal{$section} += $cust_bill_pkg->recur
3276 if $cust_bill_pkg->recur != 0;
3279 if ($type && $type eq 'R') {
3280 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3281 if $cust_bill_pkg->recur != 0;
3284 if ($type && $type eq 'U') {
3285 $subtotal{$section} += $usage
3286 unless scalar(@$extra_sections);
3295 %pkg_category_cache = ();
3297 push @$late, map { { 'description' => &{$escape}($_),
3298 'subtotal' => $late_subtotal{$_},
3300 'sort_weight' => ( _pkg_category($_)
3301 ? _pkg_category($_)->weight
3304 ((_pkg_category($_) && _pkg_category($_)->condense)
3305 ? $self->_condense_section($format)
3309 sort _sectionsort keys %late_subtotal;
3312 if ( $summarypage ) {
3313 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3314 map { $_->categoryname } qsearch('pkg_category', {});
3316 @sections = keys %subtotal;
3319 my @early = map { { 'description' => &{$escape}($_),
3320 'subtotal' => $subtotal{$_},
3321 'summarized' => $not_tax{$_} ? '' : 'Y',
3322 'tax_section' => $not_tax{$_} ? '' : 'Y',
3323 'sort_weight' => ( _pkg_category($_)
3324 ? _pkg_category($_)->weight
3327 ((_pkg_category($_) && _pkg_category($_)->condense)
3328 ? $self->_condense_section($format)
3333 push @early, @$extra_sections if $extra_sections;
3335 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3339 #helper subs for above
3342 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3346 my $categoryname = shift;
3347 $pkg_category_cache{$categoryname} ||=
3348 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3351 my %condensed_format = (
3352 'label' => [ qw( Description Qty Amount ) ],
3354 sub { shift->{description} },
3355 sub { shift->{quantity} },
3356 sub { shift->{amount} },
3358 'align' => [ qw( l r r ) ],
3359 'span' => [ qw( 5 1 1 ) ], # unitprices?
3360 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3363 sub _condense_section {
3364 my ( $self, $format ) = ( shift, shift );
3366 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3367 qw( description_generator
3370 total_line_generator
3375 sub _condensed_generator_defaults {
3376 my ( $self, $format ) = ( shift, shift );
3377 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3386 sub _condensed_header_generator {
3387 my ( $self, $format ) = ( shift, shift );
3389 my ( $f, $prefix, $suffix, $separator, $column ) =
3390 _condensed_generator_defaults($format);
3392 if ($format eq 'latex') {
3393 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3394 $suffix = "\\\\\n\\hline";
3397 sub { my ($d,$a,$s,$w) = @_;
3398 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3400 } elsif ( $format eq 'html' ) {
3401 $prefix = '<th></th>';
3405 sub { my ($d,$a,$s,$w) = @_;
3406 return qq!<th align="$html_align{$a}">$d</th>!;
3414 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3416 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3419 $prefix. join($separator, @result). $suffix;
3424 sub _condensed_description_generator {
3425 my ( $self, $format ) = ( shift, shift );
3427 my ( $f, $prefix, $suffix, $separator, $column ) =
3428 _condensed_generator_defaults($format);
3430 if ($format eq 'latex') {
3431 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3433 $separator = " & \n";
3435 sub { my ($d,$a,$s,$w) = @_;
3436 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3438 }elsif ( $format eq 'html' ) {
3439 $prefix = '"><td align="center"></td>';
3443 sub { my ($d,$a,$s,$w) = @_;
3444 return qq!<td align="$html_align{$a}">$d</td>!;
3452 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3453 push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3454 map { $f->{$_}->[$i] } qw(align span width)
3458 $prefix. join( $separator, @result ). $suffix;
3463 sub _condensed_total_generator {
3464 my ( $self, $format ) = ( shift, shift );
3466 my ( $f, $prefix, $suffix, $separator, $column ) =
3467 _condensed_generator_defaults($format);
3470 if ($format eq 'latex') {
3473 $separator = " & \n";
3475 sub { my ($d,$a,$s,$w) = @_;
3476 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3478 }elsif ( $format eq 'html' ) {
3482 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3484 sub { my ($d,$a,$s,$w) = @_;
3485 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3494 # my $r = &{$f->{fields}->[$i]}(@args);
3495 # $r .= ' Total' unless $i;
3497 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3499 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3500 map { $f->{$_}->[$i] } qw(align span width)
3504 $prefix. join( $separator, @result ). $suffix;
3509 =item total_line_generator FORMAT
3511 Returns a coderef used for generation of invoice total line items for this
3512 usage_class. FORMAT is either html or latex
3516 # should not be used: will have issues with hash element names (description vs
3517 # total_item and amount vs total_amount -- another array of functions?
3519 sub _condensed_total_line_generator {
3520 my ( $self, $format ) = ( shift, shift );
3522 my ( $f, $prefix, $suffix, $separator, $column ) =
3523 _condensed_generator_defaults($format);
3526 if ($format eq 'latex') {
3529 $separator = " & \n";
3531 sub { my ($d,$a,$s,$w) = @_;
3532 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3534 }elsif ( $format eq 'html' ) {
3538 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3540 sub { my ($d,$a,$s,$w) = @_;
3541 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3550 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3552 &{$column}( &{$f->{fields}->[$i]}(@args),
3553 map { $f->{$_}->[$i] } qw(align span width)
3557 $prefix. join( $separator, @result ). $suffix;
3562 #sub _items_extra_usage_sections {
3564 # my $escape = shift;
3566 # my %sections = ();
3568 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3569 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3571 # next unless $cust_bill_pkg->pkgnum > 0;
3573 # foreach my $section ( keys %usage_class ) {
3575 # my $usage = $cust_bill_pkg->usage($section);
3577 # next unless $usage && $usage > 0;
3579 # $sections{$section} ||= 0;
3580 # $sections{$section} += $usage;
3586 # map { { 'description' => &{$escape}($_),
3587 # 'subtotal' => $sections{$_},
3588 # 'summarized' => '',
3589 # 'tax_section' => '',
3592 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3596 sub _items_extra_usage_sections {
3605 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3606 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3607 next unless $cust_bill_pkg->pkgnum > 0;
3609 foreach my $classnum ( keys %usage_class ) {
3610 my $section = $usage_class{$classnum}->classname;
3611 $classnums{$section} = $classnum;
3613 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3614 my $amount = $detail->amount;
3615 next unless $amount && $amount > 0;
3617 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3618 $sections{$section}{amount} += $amount; #subtotal
3619 $sections{$section}{calls}++;
3620 $sections{$section}{duration} += $detail->duration;
3622 my $desc = $detail->regionname;
3623 my $description = $desc;
3624 $description = substr($desc, 0, 50). '...'
3625 if $format eq 'latex' && length($desc) > 50;
3627 $lines{$section}{$desc} ||= {
3628 description => &{$escape}($description),
3629 #pkgpart => $part_pkg->pkgpart,
3630 pkgnum => $cust_bill_pkg->pkgnum,
3635 #unit_amount => $cust_bill_pkg->unitrecur,
3636 quantity => $cust_bill_pkg->quantity,
3637 product_code => 'N/A',
3638 ext_description => [],
3641 $lines{$section}{$desc}{amount} += $amount;
3642 $lines{$section}{$desc}{calls}++;
3643 $lines{$section}{$desc}{duration} += $detail->duration;
3649 my %sectionmap = ();
3650 foreach (keys %sections) {
3651 my $usage_class = $usage_class{$classnums{$_}};
3652 $sectionmap{$_} = { 'description' => &{$escape}($_),
3653 'amount' => $sections{$_}{amount}, #subtotal
3654 'calls' => $sections{$_}{calls},
3655 'duration' => $sections{$_}{duration},
3657 'tax_section' => '',
3658 'sort_weight' => $usage_class->weight,
3659 ( $usage_class->format
3660 ? ( map { $_ => $usage_class->$_($format) }
3661 qw( description_generator header_generator total_generator total_line_generator )
3668 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3672 foreach my $section ( keys %lines ) {
3673 foreach my $line ( keys %{$lines{$section}} ) {
3674 my $l = $lines{$section}{$line};
3675 $l->{section} = $sectionmap{$section};
3676 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3677 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3682 return(\@sections, \@lines);
3686 sub _items_svc_phone_sections {
3695 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3697 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3698 next unless $cust_bill_pkg->pkgnum > 0;
3700 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3702 my $phonenum = $detail->phonenum;
3703 next unless $phonenum;
3705 my $amount = $detail->amount;
3706 next unless $amount && $amount > 0;
3708 $sections{$phonenum} ||= { 'amount' => 0,
3711 'sort_weight' => -1,
3712 'phonenum' => $phonenum,
3714 $sections{$phonenum}{amount} += $amount; #subtotal
3715 $sections{$phonenum}{calls}++;
3716 $sections{$phonenum}{duration} += $detail->duration;
3718 my $desc = $detail->regionname;
3719 my $description = $desc;
3720 $description = substr($desc, 0, 50). '...'
3721 if $format eq 'latex' && length($desc) > 50;
3723 $lines{$phonenum}{$desc} ||= {
3724 description => &{$escape}($description),
3725 #pkgpart => $part_pkg->pkgpart,
3733 product_code => 'N/A',
3734 ext_description => [],
3737 $lines{$phonenum}{$desc}{amount} += $amount;
3738 $lines{$phonenum}{$desc}{calls}++;
3739 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3741 my $line = $usage_class{$detail->classnum}->classname;
3742 $sections{"$phonenum $line"} ||=
3746 'sort_weight' => $usage_class{$detail->classnum}->weight,
3747 'phonenum' => $phonenum,
3749 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3750 $sections{"$phonenum $line"}{calls}++;
3751 $sections{"$phonenum $line"}{duration} += $detail->duration;
3753 $lines{"$phonenum $line"}{$desc} ||= {
3754 description => &{$escape}($description),
3755 #pkgpart => $part_pkg->pkgpart,
3763 product_code => 'N/A',
3764 ext_description => [],
3767 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3768 $lines{"$phonenum $line"}{$desc}{calls}++;
3769 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3770 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3771 $detail->formatted('format' => $format);
3776 my %sectionmap = ();
3777 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3778 my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3779 foreach ( keys %sections ) {
3780 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3781 my $usage_class = $summary ? $simple : $usage_simple;
3782 my $ending = $summary ? ' usage charges' : '';
3783 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3784 'amount' => $sections{$_}{amount}, #subtotal
3785 'calls' => $sections{$_}{calls},
3786 'duration' => $sections{$_}{duration},
3788 'tax_section' => '',
3789 'phonenum' => $sections{$_}{phonenum},
3790 'sort_weight' => $sections{$_}{sort_weight},
3791 'post_total' => $summary, #inspire pagebreak
3793 ( map { $_ => $usage_class->$_($format) }
3794 qw( description_generator
3797 total_line_generator
3804 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3805 $a->{sort_weight} <=> $b->{sort_weight}
3810 foreach my $section ( keys %lines ) {
3811 foreach my $line ( keys %{$lines{$section}} ) {
3812 my $l = $lines{$section}{$line};
3813 $l->{section} = $sectionmap{$section};
3814 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3815 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3820 return(\@sections, \@lines);
3827 #my @display = scalar(@_)
3829 # : qw( _items_previous _items_pkg );
3830 # #: qw( _items_pkg );
3831 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3832 my @display = qw( _items_previous _items_pkg );
3835 foreach my $display ( @display ) {
3836 push @b, $self->$display(@_);
3841 sub _items_previous {
3843 my $cust_main = $self->cust_main;
3844 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3846 foreach ( @pr_cust_bill ) {
3847 my $date = $conf->exists('invoice_show_prior_due_date')
3848 ? 'due '. $_->due_date2str($date_format)
3849 : time2str($date_format, $_->_date);
3851 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3852 #'pkgpart' => 'N/A',
3854 'amount' => sprintf("%.2f", $_->owed),
3860 # 'description' => 'Previous Balance',
3861 # #'pkgpart' => 'N/A',
3862 # 'pkgnum' => 'N/A',
3863 # 'amount' => sprintf("%10.2f", $pr_total ),
3864 # 'ext_description' => [ map {
3865 # "Invoice ". $_->invnum.
3866 # " (". time2str("%x",$_->_date). ") ".
3867 # sprintf("%10.2f", $_->owed)
3868 # } @pr_cust_bill ],
3876 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3877 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3878 if ($options{section} && $options{section}->{condensed}) {
3880 local $Storable::canonical = 1;
3881 foreach ( @items ) {
3883 delete $item->{ref};
3884 delete $item->{ext_description};
3885 my $key = freeze($item);
3886 $itemshash{$key} ||= 0;
3887 $itemshash{$key} ++; # += $item->{quantity};
3889 @items = sort { $a->{description} cmp $b->{description} }
3890 map { my $i = thaw($_);
3891 $i->{quantity} = $itemshash{$_};
3893 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3902 return 0 unless $a cmp $b;
3903 return -1 if $b eq 'Tax';
3904 return 1 if $a eq 'Tax';
3905 return -1 if $b eq 'Other surcharges';
3906 return 1 if $a eq 'Other surcharges';
3912 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3913 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3916 sub _items_cust_bill_pkg {
3918 my $cust_bill_pkg = shift;
3921 my $format = $opt{format} || '';
3922 my $escape_function = $opt{escape_function} || sub { shift };
3923 my $format_function = $opt{format_function} || '';
3924 my $unsquelched = $opt{unsquelched} || '';
3925 my $section = $opt{section}->{description} if $opt{section};
3926 my $summary_page = $opt{summary_page} || '';
3927 my $multilocation = $opt{multilocation} || '';
3930 my ($s, $r, $u) = ( undef, undef, undef );
3931 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3934 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3935 if ( $_ && !$cust_bill_pkg->hidden ) {
3936 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3937 $_->{amount} =~ s/^\-0\.00$/0.00/;
3938 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3940 unless $_->{amount} == 0;
3945 foreach my $display ( grep { defined($section)
3946 ? $_->section eq $section
3949 grep { !$_->summary || !$summary_page }
3950 $cust_bill_pkg->cust_bill_pkg_display
3954 my $type = $display->type;
3956 my $desc = $cust_bill_pkg->desc;
3957 $desc = substr($desc, 0, 50). '...'
3958 if $format eq 'latex' && length($desc) > 50;
3960 my %details_opt = ( 'format' => $format,
3961 'escape_function' => $escape_function,
3962 'format_function' => $format_function,
3965 if ( $cust_bill_pkg->pkgnum > 0 ) {
3967 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3969 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3971 my $description = $desc;
3972 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3975 unless ( $cust_pkg->part_pkg->hide_svc_detail
3976 || $cust_bill_pkg->hidden )
3978 push @d, map &{$escape_function}($_),
3979 $cust_pkg->h_labels_short($self->_date);
3980 if ( $multilocation ) {
3981 my $loc = $cust_pkg->location_label;
3982 $loc = substr($desc, 0, 50). '...'
3983 if $format eq 'latex' && length($loc) > 50;
3984 push @d, &{$escape_function}($loc);
3987 push @d, $cust_bill_pkg->details(%details_opt)
3988 if $cust_bill_pkg->recur == 0;
3990 if ( $cust_bill_pkg->hidden ) {
3991 $s->{amount} += $cust_bill_pkg->setup;
3992 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3993 push @{ $s->{ext_description} }, @d;
3996 description => $description,
3997 #pkgpart => $part_pkg->pkgpart,
3998 pkgnum => $cust_bill_pkg->pkgnum,
3999 amount => $cust_bill_pkg->setup,
4000 unit_amount => $cust_bill_pkg->unitsetup,
4001 quantity => $cust_bill_pkg->quantity,
4002 ext_description => \@d,
4008 if ( $cust_bill_pkg->recur != 0 &&
4009 ( !$type || $type eq 'R' || $type eq 'U' )
4013 my $is_summary = $display->summary;
4014 my $description = ($is_summary && $type && $type eq 'U')
4015 ? "Usage charges" : $desc;
4017 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4018 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4019 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4024 #at least until cust_bill_pkg has "past" ranges in addition to
4025 #the "future" sdate/edate ones... see #3032
4026 my @dates = ( $self->_date );
4027 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4028 push @dates, $prev->sdate if $prev;
4030 unless ( $cust_pkg->part_pkg->hide_svc_detail
4031 || $cust_bill_pkg->itemdesc
4032 || $cust_bill_pkg->hidden
4033 || $is_summary && $type && $type eq 'U' )
4035 push @d, map &{$escape_function}($_),
4036 $cust_pkg->h_labels_short(@dates)
4037 #$cust_bill_pkg->edate,
4038 #$cust_bill_pkg->sdate)
4040 if ( $multilocation ) {
4041 my $loc = $cust_pkg->location_label;
4042 $loc = substr($desc, 0, 50). '...'
4043 if $format eq 'latex' && length($loc) > 50;
4044 push @d, &{$escape_function}($loc);
4048 push @d, $cust_bill_pkg->details(%details_opt)
4049 unless ($is_summary || $type && $type eq 'R');
4053 $amount = $cust_bill_pkg->recur;
4054 }elsif($type eq 'R') {
4055 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4056 }elsif($type eq 'U') {
4057 $amount = $cust_bill_pkg->usage;
4060 if ( !$type || $type eq 'R' ) {
4062 if ( $cust_bill_pkg->hidden ) {
4063 $r->{amount} += $amount;
4064 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4065 push @{ $r->{ext_description} }, @d;
4068 description => $description,
4069 #pkgpart => $part_pkg->pkgpart,
4070 pkgnum => $cust_bill_pkg->pkgnum,
4072 unit_amount => $cust_bill_pkg->unitrecur,
4073 quantity => $cust_bill_pkg->quantity,
4074 ext_description => \@d,
4078 } elsif ( $amount ) { # && $type eq 'U'
4080 if ( $cust_bill_pkg->hidden ) {
4081 $u->{amount} += $amount;
4082 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4083 push @{ $u->{ext_description} }, @d;
4086 description => $description,
4087 #pkgpart => $part_pkg->pkgpart,
4088 pkgnum => $cust_bill_pkg->pkgnum,
4090 unit_amount => $cust_bill_pkg->unitrecur,
4091 quantity => $cust_bill_pkg->quantity,
4092 ext_description => \@d,
4098 } # recurring or usage with recurring charge
4100 } else { #pkgnum tax or one-shot line item (??)
4102 if ( $cust_bill_pkg->setup != 0 ) {
4104 'description' => $desc,
4105 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4108 if ( $cust_bill_pkg->recur != 0 ) {
4110 'description' => "$desc (".
4111 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4112 time2str($date_format, $cust_bill_pkg->edate). ')',
4113 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4123 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4125 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4126 $_->{amount} =~ s/^\-0\.00$/0.00/;
4127 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4129 unless $_->{amount} == 0;
4137 sub _items_credits {
4138 my( $self, %opt ) = @_;
4139 my $trim_len = $opt{'trim_len'} || 60;
4143 foreach ( $self->cust_credited ) {
4145 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4147 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4148 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4149 $reason = " ($reason) " if $reason;
4152 #'description' => 'Credit ref\#'. $_->crednum.
4153 # " (". time2str("%x",$_->cust_credit->_date) .")".
4155 'description' => 'Credit applied '.
4156 time2str($date_format,$_->cust_credit->_date). $reason,
4157 'amount' => sprintf("%.2f",$_->amount),
4165 sub _items_payments {
4169 #get & print payments
4170 foreach ( $self->cust_bill_pay ) {
4172 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4175 'description' => "Payment received ".
4176 time2str($date_format,$_->cust_pay->_date ),
4177 'amount' => sprintf("%.2f", $_->amount )
4185 =item call_details [ OPTION => VALUE ... ]
4187 Returns an array of CSV strings representing the call details for this invoice
4188 The only option available is the boolean prepend_billed_number
4193 my ($self, %opt) = @_;
4195 my $format_function = sub { shift };
4197 if ($opt{prepend_billed_number}) {
4198 $format_function = sub {
4202 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4207 my @details = map { $_->details( 'format_function' => $format_function,
4208 'escape_function' => sub{ return() },
4212 $self->cust_bill_pkg;
4213 my $header = $details[0];
4214 ( $header, grep { $_ ne $header } @details );
4224 =item process_reprint
4228 sub process_reprint {
4229 process_re_X('print', @_);
4232 =item process_reemail
4236 sub process_reemail {
4237 process_re_X('email', @_);
4245 process_re_X('fax', @_);
4253 process_re_X('ftp', @_);
4260 sub process_respool {
4261 process_re_X('spool', @_);
4264 use Storable qw(thaw);
4268 my( $method, $job ) = ( shift, shift );
4269 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4271 my $param = thaw(decode_base64(shift));
4272 warn Dumper($param) if $DEBUG;
4283 my($method, $job, %param ) = @_;
4285 warn "re_X $method for job $job with param:\n".
4286 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4289 #some false laziness w/search/cust_bill.html
4291 my $orderby = 'ORDER BY cust_bill._date';
4293 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4295 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4297 my @cust_bill = qsearch( {
4298 #'select' => "cust_bill.*",
4299 'table' => 'cust_bill',
4300 'addl_from' => $addl_from,
4302 'extra_sql' => $extra_sql,
4303 'order_by' => $orderby,
4307 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4309 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4312 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4313 foreach my $cust_bill ( @cust_bill ) {
4314 $cust_bill->$method();
4316 if ( $job ) { #progressbar foo
4318 if ( time - $min_sec > $last ) {
4319 my $error = $job->update_statustext(
4320 int( 100 * $num / scalar(@cust_bill) )
4322 die $error if $error;
4333 =head1 CLASS METHODS
4339 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4344 my ($class, $start, $end) = @_;
4346 $class->paid_sql($start, $end). ' - '.
4347 $class->credited_sql($start, $end);
4352 Returns an SQL fragment to retreive the net amount (charged minus credited).
4357 my ($class, $start, $end) = @_;
4358 'charged - '. $class->credited_sql($start, $end);
4363 Returns an SQL fragment to retreive the amount paid against this invoice.
4368 my ($class, $start, $end) = @_;
4369 $start &&= "AND cust_bill_pay._date <= $start";
4370 $end &&= "AND cust_bill_pay._date > $end";
4371 $start = '' unless defined($start);
4372 $end = '' unless defined($end);
4373 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4374 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4379 Returns an SQL fragment to retreive the amount credited against this invoice.
4384 my ($class, $start, $end) = @_;
4385 $start &&= "AND cust_credit_bill._date <= $start";
4386 $end &&= "AND cust_credit_bill._date > $end";
4387 $start = '' unless defined($start);
4388 $end = '' unless defined($end);
4389 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4390 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4393 =item search_sql_where HASHREF
4395 Class method which returns an SQL WHERE fragment to search for parameters
4396 specified in HASHREF. Valid parameters are
4402 List reference of start date, end date, as UNIX timestamps.
4412 List reference of charged limits (exclusive).
4416 List reference of charged limits (exclusive).
4420 flag, return open invoices only
4424 flag, return net invoices only
4428 =item newest_percust
4432 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4436 sub search_sql_where {
4437 my($class, $param) = @_;
4439 warn "$me search_sql_where called with params: \n".
4440 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4446 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4447 push @search, "cust_main.agentnum = $1";
4451 if ( $param->{_date} ) {
4452 my($beginning, $ending) = @{$param->{_date}};
4454 push @search, "cust_bill._date >= $beginning",
4455 "cust_bill._date < $ending";
4459 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4460 push @search, "cust_bill.invnum >= $1";
4462 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4463 push @search, "cust_bill.invnum <= $1";
4467 if ( $param->{charged} ) {
4468 my @charged = ref($param->{charged})
4469 ? @{ $param->{charged} }
4470 : ($param->{charged});
4472 push @search, map { s/^charged/cust_bill.charged/; $_; }
4476 my $owed_sql = FS::cust_bill->owed_sql;
4479 if ( $param->{owed} ) {
4480 my @owed = ref($param->{owed})
4481 ? @{ $param->{owed} }
4483 push @search, map { s/^owed/$owed_sql/; $_; }
4488 push @search, "0 != $owed_sql"
4489 if $param->{'open'};
4490 push @search, '0 != '. FS::cust_bill->net_sql
4494 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4495 if $param->{'days'};
4498 if ( $param->{'newest_percust'} ) {
4500 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4501 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4503 my @newest_where = map { my $x = $_;
4504 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4507 grep ! /^cust_main./, @search;
4508 my $newest_where = scalar(@newest_where)
4509 ? ' AND '. join(' AND ', @newest_where)
4513 push @search, "cust_bill._date = (
4514 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4515 WHERE newest_cust_bill.custnum = cust_bill.custnum
4521 #agent virtualization
4522 my $curuser = $FS::CurrentUser::CurrentUser;
4523 if ( $curuser->username eq 'fs_queue'
4524 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4526 my $newuser = qsearchs('access_user', {
4527 'username' => $username,
4531 $curuser = $newuser;
4533 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4536 push @search, $curuser->agentnums_sql;
4538 join(' AND ', @search );
4550 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4551 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base