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 { $_->cust_pkg } $self->cust_bill_pkg;
361 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
364 =item open_cust_bill_pkg
366 Returns the open line items for this invoice.
368 Note that cust_bill_pkg with both setup and recur fees are returned as two
369 separate line items, each with only one fee.
373 # modeled after cust_main::open_cust_bill
374 sub open_cust_bill_pkg {
377 # grep { $_->owed > 0 } $self->cust_bill_pkg
379 my %other = ( 'recur' => 'setup',
380 'setup' => 'recur', );
382 foreach my $field ( qw( recur setup )) {
383 push @open, map { $_->set( $other{$field}, 0 ); $_; }
384 grep { $_->owed($field) > 0 }
385 $self->cust_bill_pkg;
391 =item cust_bill_event
393 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
397 sub cust_bill_event {
399 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
402 =item num_cust_bill_event
404 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
408 sub num_cust_bill_event {
411 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
412 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
413 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
414 $sth->fetchrow_arrayref->[0];
419 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
423 #false laziness w/cust_pkg.pm
427 'table' => 'cust_event',
428 'addl_from' => 'JOIN part_event USING ( eventpart )',
429 'hashref' => { 'tablenum' => $self->invnum },
430 'extra_sql' => " AND eventtable = 'cust_bill' ",
436 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
440 #false laziness w/cust_pkg.pm
444 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
445 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
446 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
447 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
448 $sth->fetchrow_arrayref->[0];
453 Returns the customer (see L<FS::cust_main>) for this invoice.
459 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
462 =item cust_suspend_if_balance_over AMOUNT
464 Suspends the customer associated with this invoice if the total amount owed on
465 this invoice and all older invoices is greater than the specified amount.
467 Returns a list: an empty list on success or a list of errors.
471 sub cust_suspend_if_balance_over {
472 my( $self, $amount ) = ( shift, shift );
473 my $cust_main = $self->cust_main;
474 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
477 $cust_main->suspend(@_);
483 Depreciated. See the cust_credited method.
485 #Returns a list consisting of the total previous credited (see
486 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
487 #outstanding credits (FS::cust_credit objects).
493 croak "FS::cust_bill->cust_credit depreciated; see ".
494 "FS::cust_bill->cust_credit_bill";
497 #my @cust_credit = sort { $a->_date <=> $b->_date }
498 # grep { $_->credited != 0 && $_->_date < $self->_date }
499 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
501 #foreach (@cust_credit) { $total += $_->credited; }
502 #$total, @cust_credit;
507 Depreciated. See the cust_bill_pay method.
509 #Returns all payments (see L<FS::cust_pay>) for this invoice.
515 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
517 #sort { $a->_date <=> $b->_date }
518 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
524 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
527 sub cust_bill_pay_batch {
529 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
534 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
540 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
541 sort { $a->_date <=> $b->_date }
542 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
547 =item cust_credit_bill
549 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
555 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
556 sort { $a->_date <=> $b->_date }
557 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
561 sub cust_credit_bill {
562 shift->cust_credited(@_);
565 =item cust_bill_pay_pkgnum PKGNUM
567 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
568 with matching pkgnum.
572 sub cust_bill_pay_pkgnum {
573 my( $self, $pkgnum ) = @_;
574 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
575 sort { $a->_date <=> $b->_date }
576 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
582 =item cust_credited_pkgnum PKGNUM
584 =item cust_credit_bill_pkgnum PKGNUM
586 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
587 with matching pkgnum.
591 sub cust_credited_pkgnum {
592 my( $self, $pkgnum ) = @_;
593 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
594 sort { $a->_date <=> $b->_date }
595 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
601 sub cust_credit_bill_pkgnum {
602 shift->cust_credited_pkgnum(@_);
607 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
614 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
616 foreach (@taxlines) { $total += $_->setup; }
622 Returns the amount owed (still outstanding) on this invoice, which is charged
623 minus all payment applications (see L<FS::cust_bill_pay>) and credit
624 applications (see L<FS::cust_credit_bill>).
630 my $balance = $self->charged;
631 $balance -= $_->amount foreach ( $self->cust_bill_pay );
632 $balance -= $_->amount foreach ( $self->cust_credited );
633 $balance = sprintf( "%.2f", $balance);
634 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
639 my( $self, $pkgnum ) = @_;
641 #my $balance = $self->charged;
643 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
645 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
646 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
648 $balance = sprintf( "%.2f", $balance);
649 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
653 =item apply_payments_and_credits [ OPTION => VALUE ... ]
655 Applies unapplied payments and credits to this invoice.
657 A hash of optional arguments may be passed. Currently "manual" is supported.
658 If true, a payment receipt is sent instead of a statement when
659 'payment_receipt_email' configuration option is set.
661 If there is an error, returns the error, otherwise returns false.
665 sub apply_payments_and_credits {
666 my( $self, %options ) = @_;
668 local $SIG{HUP} = 'IGNORE';
669 local $SIG{INT} = 'IGNORE';
670 local $SIG{QUIT} = 'IGNORE';
671 local $SIG{TERM} = 'IGNORE';
672 local $SIG{TSTP} = 'IGNORE';
673 local $SIG{PIPE} = 'IGNORE';
675 my $oldAutoCommit = $FS::UID::AutoCommit;
676 local $FS::UID::AutoCommit = 0;
679 $self->select_for_update; #mutex
681 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
682 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
684 if ( $conf->exists('pkg-balances') ) {
685 # limit @payments & @credits to those w/ a pkgnum grepped from $self
686 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
687 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
688 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
691 while ( $self->owed > 0 and ( @payments || @credits ) ) {
694 if ( @payments && @credits ) {
696 #decide which goes first by weight of top (unapplied) line item
698 my @open_lineitems = $self->open_cust_bill_pkg;
701 max( map { $_->part_pkg->pay_weight || 0 }
706 my $max_credit_weight =
707 max( map { $_->part_pkg->credit_weight || 0 }
713 #if both are the same... payments first? it has to be something
714 if ( $max_pay_weight >= $max_credit_weight ) {
720 } elsif ( @payments ) {
722 } elsif ( @credits ) {
725 die "guru meditation #12 and 35";
729 if ( $app eq 'pay' ) {
731 my $payment = shift @payments;
732 $unapp_amount = $payment->unapplied;
733 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
734 $app->pkgnum( $payment->pkgnum )
735 if $conf->exists('pkg-balances') && $payment->pkgnum;
737 } elsif ( $app eq 'credit' ) {
739 my $credit = shift @credits;
740 $unapp_amount = $credit->credited;
741 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
742 $app->pkgnum( $credit->pkgnum )
743 if $conf->exists('pkg-balances') && $credit->pkgnum;
746 die "guru meditation #12 and 35";
750 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
751 warn "owed_pkgnum ". $app->pkgnum;
752 $owed = $self->owed_pkgnum($app->pkgnum);
756 next unless $owed > 0;
758 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
759 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
761 $app->invnum( $self->invnum );
763 my $error = $app->insert(%options);
765 $dbh->rollback if $oldAutoCommit;
766 return "Error inserting ". $app->table. " record: $error";
768 die $error if $error;
772 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
777 =item generate_email OPTION => VALUE ...
785 sender address, required
789 alternate template name, optional
793 text attachment arrayref, optional
797 email subject, optional
801 notice name instead of "Invoice", optional
805 Returns an argument list to be passed to L<FS::Misc::send_email>.
816 my $me = '[FS::cust_bill::generate_email]';
819 'from' => $args{'from'},
820 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
824 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
825 'template' => $args{'template'},
826 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
829 my $cust_main = $self->cust_main;
831 if (ref($args{'to'}) eq 'ARRAY') {
832 $return{'to'} = $args{'to'};
834 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
835 $cust_main->invoicing_list
839 if ( $conf->exists('invoice_html') ) {
841 warn "$me creating HTML/text multipart message"
844 $return{'nobody'} = 1;
846 my $alternative = build MIME::Entity
847 'Type' => 'multipart/alternative',
848 'Encoding' => '7bit',
849 'Disposition' => 'inline'
853 if ( $conf->exists('invoice_email_pdf')
854 and scalar($conf->config('invoice_email_pdf_note')) ) {
856 warn "$me using 'invoice_email_pdf_note' in multipart message"
858 $data = [ map { $_ . "\n" }
859 $conf->config('invoice_email_pdf_note')
864 warn "$me not using 'invoice_email_pdf_note' in multipart message"
866 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
867 $data = $args{'print_text'};
869 $data = [ $self->print_text(\%opt) ];
874 $alternative->attach(
875 'Type' => 'text/plain',
876 #'Encoding' => 'quoted-printable',
877 'Encoding' => '7bit',
879 'Disposition' => 'inline',
882 $args{'from'} =~ /\@([\w\.\-]+)/;
883 my $from = $1 || 'example.com';
884 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
887 my $agentnum = $cust_main->agentnum;
888 if ( defined($args{'template'}) && length($args{'template'})
889 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
892 $logo = 'logo_'. $args{'template'}. '.png';
896 my $image_data = $conf->config_binary( $logo, $agentnum);
898 my $image = build MIME::Entity
899 'Type' => 'image/png',
900 'Encoding' => 'base64',
901 'Data' => $image_data,
902 'Filename' => 'logo.png',
903 'Content-ID' => "<$content_id>",
906 $alternative->attach(
907 'Type' => 'text/html',
908 'Encoding' => 'quoted-printable',
909 'Data' => [ '<html>',
912 ' '. encode_entities($return{'subject'}),
915 ' <body bgcolor="#e8e8e8">',
916 $self->print_html({ 'cid'=>$content_id, %opt }),
920 'Disposition' => 'inline',
921 #'Filename' => 'invoice.pdf',
925 if ( $cust_main->email_csv_cdr ) {
927 push @otherparts, build MIME::Entity
928 'Type' => 'text/csv',
929 'Encoding' => '7bit',
930 'Data' => [ map { "$_\n" }
931 $self->call_details('prepend_billed_number' => 1)
933 'Disposition' => 'attachment',
934 'Filename' => 'usage-'. $self->invnum. '.csv',
939 if ( $conf->exists('invoice_email_pdf') ) {
944 # multipart/alternative
950 my $related = build MIME::Entity 'Type' => 'multipart/related',
951 'Encoding' => '7bit';
953 #false laziness w/Misc::send_email
954 $related->head->replace('Content-type',
956 '; boundary="'. $related->head->multipart_boundary. '"'.
957 '; type=multipart/alternative'
960 $related->add_part($alternative);
962 $related->add_part($image);
964 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
966 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
970 #no other attachment:
972 # multipart/alternative
977 $return{'content-type'} = 'multipart/related';
978 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
979 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
980 #$return{'disposition'} = 'inline';
986 if ( $conf->exists('invoice_email_pdf') ) {
987 warn "$me creating PDF attachment"
990 #mime parts arguments a la MIME::Entity->build().
991 $return{'mimeparts'} = [
992 { $self->mimebuild_pdf(\%opt) }
996 if ( $conf->exists('invoice_email_pdf')
997 and scalar($conf->config('invoice_email_pdf_note')) ) {
999 warn "$me using 'invoice_email_pdf_note'"
1001 $return{'body'} = [ map { $_ . "\n" }
1002 $conf->config('invoice_email_pdf_note')
1007 warn "$me not using 'invoice_email_pdf_note'"
1009 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1010 $return{'body'} = $args{'print_text'};
1012 $return{'body'} = [ $self->print_text(\%opt) ];
1025 Returns a list suitable for passing to MIME::Entity->build(), representing
1026 this invoice as PDF attachment.
1033 'Type' => 'application/pdf',
1034 'Encoding' => 'base64',
1035 'Data' => [ $self->print_pdf(@_) ],
1036 'Disposition' => 'attachment',
1037 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1041 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1043 Sends this invoice to the destinations configured for this customer: sends
1044 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1046 Options can be passed as a hashref (recommended) or as a list of up to
1047 four values for templatename, agentnum, invoice_from and amount.
1049 I<template>, if specified, is the name of a suffix for alternate invoices.
1051 I<agentnum>, if specified, means that this invoice will only be sent for customers
1052 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1053 single agent) or an arrayref of agentnums.
1055 I<invoice_from>, if specified, overrides the default email invoice From: address.
1057 I<amount>, if specified, only sends the invoice if the total amount owed on this
1058 invoice and all older invoices is greater than the specified amount.
1060 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1064 sub queueable_send {
1067 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1068 or die "invalid invoice number: " . $opt{invnum};
1070 my @args = ( $opt{template}, $opt{agentnum} );
1071 push @args, $opt{invoice_from}
1072 if exists($opt{invoice_from}) && $opt{invoice_from};
1074 my $error = $self->send( @args );
1075 die $error if $error;
1082 my( $template, $invoice_from, $notice_name );
1084 my $balance_over = 0;
1088 $template = $opt->{'template'} || '';
1089 if ( $agentnums = $opt->{'agentnum'} ) {
1090 $agentnums = [ $agentnums ] unless ref($agentnums);
1092 $invoice_from = $opt->{'invoice_from'};
1093 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1094 $notice_name = $opt->{'notice_name'};
1096 $template = scalar(@_) ? shift : '';
1097 if ( scalar(@_) && $_[0] ) {
1098 $agentnums = ref($_[0]) ? shift : [ shift ];
1100 $invoice_from = shift if scalar(@_);
1101 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1104 return 'N/A' unless ! $agentnums
1105 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1108 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1110 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1111 $conf->config('invoice_from', $self->cust_main->agentnum );
1114 'template' => $template,
1115 'invoice_from' => $invoice_from,
1116 'notice_name' => ( $notice_name || 'Invoice' ),
1119 my @invoicing_list = $self->cust_main->invoicing_list;
1121 #$self->email_invoice(\%opt)
1123 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1125 #$self->print_invoice(\%opt)
1127 if grep { $_ eq 'POST' } @invoicing_list; #postal
1129 $self->fax_invoice(\%opt)
1130 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1136 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1138 Emails this invoice.
1140 Options can be passed as a hashref (recommended) or as a list of up to
1141 two values for templatename and invoice_from.
1143 I<template>, if specified, is the name of a suffix for alternate invoices.
1145 I<invoice_from>, if specified, overrides the default email invoice From: address.
1147 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1151 sub queueable_email {
1154 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1155 or die "invalid invoice number: " . $opt{invnum};
1157 my @args = ( $opt{template} );
1158 push @args, $opt{invoice_from}
1159 if exists($opt{invoice_from}) && $opt{invoice_from};
1161 my $error = $self->email( @args );
1162 die $error if $error;
1166 #sub email_invoice {
1170 my( $template, $invoice_from, $notice_name );
1173 $template = $opt->{'template'} || '';
1174 $invoice_from = $opt->{'invoice_from'};
1175 $notice_name = $opt->{'notice_name'} || 'Invoice';
1177 $template = scalar(@_) ? shift : '';
1178 $invoice_from = shift if scalar(@_);
1179 $notice_name = 'Invoice';
1182 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1183 $conf->config('invoice_from', $self->cust_main->agentnum );
1185 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1186 $self->cust_main->invoicing_list;
1188 #better to notify this person than silence
1189 @invoicing_list = ($invoice_from) unless @invoicing_list;
1191 my $subject = $self->email_subject($template);
1193 my $error = send_email(
1194 $self->generate_email(
1195 'from' => $invoice_from,
1196 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1197 'subject' => $subject,
1198 'template' => $template,
1199 'notice_name' => $notice_name,
1202 die "can't email invoice: $error\n" if $error;
1203 #die "$error\n" if $error;
1210 #my $template = scalar(@_) ? shift : '';
1213 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1216 my $cust_main = $self->cust_main;
1217 my $name = $cust_main->name;
1218 my $name_short = $cust_main->name_short;
1219 my $invoice_number = $self->invnum;
1220 my $invoice_date = $self->_date_pretty;
1222 eval qq("$subject");
1225 =item lpr_data HASHREF | [ TEMPLATE ]
1227 Returns the postscript or plaintext for this invoice as an arrayref.
1229 Options can be passed as a hashref (recommended) or as a single optional value
1232 I<template>, if specified, is the name of a suffix for alternate invoices.
1234 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1240 my( $template, $notice_name );
1243 $template = $opt->{'template'} || '';
1244 $notice_name = $opt->{'notice_name'} || 'Invoice';
1246 $template = scalar(@_) ? shift : '';
1247 $notice_name = 'Invoice';
1251 'template' => $template,
1252 'notice_name' => $notice_name,
1255 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1256 [ $self->$method( \%opt ) ];
1259 =item print HASHREF | [ TEMPLATE ]
1261 Prints this invoice.
1263 Options can be passed as a hashref (recommended) or as a single optional
1266 I<template>, if specified, is the name of a suffix for alternate invoices.
1268 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1272 #sub print_invoice {
1275 my( $template, $notice_name );
1278 $template = $opt->{'template'} || '';
1279 $notice_name = $opt->{'notice_name'} || 'Invoice';
1281 $template = scalar(@_) ? shift : '';
1282 $notice_name = 'Invoice';
1286 'template' => $template,
1287 'notice_name' => $notice_name,
1290 do_print $self->lpr_data(\%opt);
1293 =item fax_invoice HASHREF | [ TEMPLATE ]
1297 Options can be passed as a hashref (recommended) or as a single optional
1300 I<template>, if specified, is the name of a suffix for alternate invoices.
1302 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1308 my( $template, $notice_name );
1311 $template = $opt->{'template'} || '';
1312 $notice_name = $opt->{'notice_name'} || 'Invoice';
1314 $template = scalar(@_) ? shift : '';
1315 $notice_name = 'Invoice';
1318 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1319 unless $conf->exists('invoice_latex');
1321 my $dialstring = $self->cust_main->getfield('fax');
1325 'template' => $template,
1326 'notice_name' => $notice_name,
1329 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1330 'dialstring' => $dialstring,
1332 die $error if $error;
1336 =item ftp_invoice [ TEMPLATENAME ]
1338 Sends this invoice data via FTP.
1340 TEMPLATENAME is unused?
1346 my $template = scalar(@_) ? shift : '';
1349 'protocol' => 'ftp',
1350 'server' => $conf->config('cust_bill-ftpserver'),
1351 'username' => $conf->config('cust_bill-ftpusername'),
1352 'password' => $conf->config('cust_bill-ftppassword'),
1353 'dir' => $conf->config('cust_bill-ftpdir'),
1354 'format' => $conf->config('cust_bill-ftpformat'),
1358 =item spool_invoice [ TEMPLATENAME ]
1360 Spools this invoice data (see L<FS::spool_csv>)
1362 TEMPLATENAME is unused?
1368 my $template = scalar(@_) ? shift : '';
1371 'format' => $conf->config('cust_bill-spoolformat'),
1372 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1376 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1378 Like B<send>, but only sends the invoice if it is the newest open invoice for
1383 sub send_if_newest {
1388 grep { $_->owed > 0 }
1389 qsearch('cust_bill', {
1390 'custnum' => $self->custnum,
1391 #'_date' => { op=>'>', value=>$self->_date },
1392 'invnum' => { op=>'>', value=>$self->invnum },
1399 =item send_csv OPTION => VALUE, ...
1401 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1405 protocol - currently only "ftp"
1411 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1412 and YYMMDDHHMMSS is a timestamp.
1414 See L</print_csv> for a description of the output format.
1419 my($self, %opt) = @_;
1423 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1424 mkdir $spooldir, 0700 unless -d $spooldir;
1426 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1427 my $file = "$spooldir/$tracctnum.csv";
1429 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1431 open(CSV, ">$file") or die "can't open $file: $!";
1439 if ( $opt{protocol} eq 'ftp' ) {
1440 eval "use Net::FTP;";
1442 $net = Net::FTP->new($opt{server}) or die @$;
1444 die "unknown protocol: $opt{protocol}";
1447 $net->login( $opt{username}, $opt{password} )
1448 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1450 $net->binary or die "can't set binary mode";
1452 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1454 $net->put($file) or die "can't put $file: $!";
1464 Spools CSV invoice data.
1470 =item format - 'default' or 'billco'
1472 =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>).
1474 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1476 =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.
1483 my($self, %opt) = @_;
1485 my $cust_main = $self->cust_main;
1487 if ( $opt{'dest'} ) {
1488 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1489 $cust_main->invoicing_list;
1490 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1491 || ! keys %invoicing_list;
1494 if ( $opt{'balanceover'} ) {
1496 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1499 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1500 mkdir $spooldir, 0700 unless -d $spooldir;
1502 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1506 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1507 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1510 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1512 open(CSV, ">>$file") or die "can't open $file: $!";
1513 flock(CSV, LOCK_EX);
1518 if ( lc($opt{'format'}) eq 'billco' ) {
1520 flock(CSV, LOCK_UN);
1525 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1528 open(CSV,">>$file") or die "can't open $file: $!";
1529 flock(CSV, LOCK_EX);
1535 flock(CSV, LOCK_UN);
1542 =item print_csv OPTION => VALUE, ...
1544 Returns CSV data for this invoice.
1548 format - 'default' or 'billco'
1550 Returns a list consisting of two scalars. The first is a single line of CSV
1551 header information for this invoice. The second is one or more lines of CSV
1552 detail information for this invoice.
1554 If I<format> is not specified or "default", the fields of the CSV file are as
1557 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1561 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1563 B<record_type> is C<cust_bill> for the initial header line only. The
1564 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1565 fields are filled in.
1567 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1568 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1571 =item invnum - invoice number
1573 =item custnum - customer number
1575 =item _date - invoice date
1577 =item charged - total invoice amount
1579 =item first - customer first name
1581 =item last - customer first name
1583 =item company - company name
1585 =item address1 - address line 1
1587 =item address2 - address line 1
1597 =item pkg - line item description
1599 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1601 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1603 =item sdate - start date for recurring fee
1605 =item edate - end date for recurring fee
1609 If I<format> is "billco", the fields of the header CSV file are as follows:
1611 +-------------------------------------------------------------------+
1612 | FORMAT HEADER FILE |
1613 |-------------------------------------------------------------------|
1614 | Field | Description | Name | Type | Width |
1615 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1616 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1617 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1618 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1619 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1620 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1621 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1622 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1623 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1624 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1625 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1626 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1627 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1628 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1629 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1630 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1631 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1632 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1633 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1634 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1635 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1636 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1637 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1638 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1639 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1640 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1641 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1642 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1643 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1644 +-------+-------------------------------+------------+------+-------+
1646 If I<format> is "billco", the fields of the detail CSV file are as follows:
1648 FORMAT FOR DETAIL FILE
1650 Field | Description | Name | Type | Width
1651 1 | N/A-Leave Empty | RC | CHAR | 2
1652 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1653 3 | Account Number | TRACCTNUM | CHAR | 15
1654 4 | Invoice Number | TRINVOICE | CHAR | 15
1655 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1656 6 | Transaction Detail | DETAILS | CHAR | 100
1657 7 | Amount | AMT | NUM* | 9
1658 8 | Line Format Control** | LNCTRL | CHAR | 2
1659 9 | Grouping Code | GROUP | CHAR | 2
1660 10 | User Defined | ACCT CODE | CHAR | 15
1665 my($self, %opt) = @_;
1667 eval "use Text::CSV_XS";
1670 my $cust_main = $self->cust_main;
1672 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1674 if ( lc($opt{'format'}) eq 'billco' ) {
1677 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1679 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1681 my( $previous_balance, @unused ) = $self->previous; #previous balance
1683 my $pmt_cr_applied = 0;
1684 $pmt_cr_applied += $_->{'amount'}
1685 foreach ( $self->_items_payments, $self->_items_credits ) ;
1687 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1690 '', # 1 | N/A-Leave Empty CHAR 2
1691 '', # 2 | N/A-Leave Empty CHAR 15
1692 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1693 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1694 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1695 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1696 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1697 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1698 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1699 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1700 '', # 10 | Ancillary Billing Information CHAR 30
1701 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1702 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1705 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1708 $duedate, # 14 | Bill Due Date CHAR 10
1710 $previous_balance, # 15 | Previous Balance NUM* 9
1711 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1712 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1713 $totaldue, # 18 | Total Amt Due NUM* 9
1714 $totaldue, # 19 | Total Amt Due NUM* 9
1715 '', # 20 | 30 Day Aging NUM* 9
1716 '', # 21 | 60 Day Aging NUM* 9
1717 '', # 22 | 90 Day Aging NUM* 9
1718 'N', # 23 | Y/N CHAR 1
1719 '', # 24 | Remittance automation CHAR 100
1720 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1721 $self->custnum, # 26 | Customer Reference Number CHAR 15
1722 '0', # 27 | Federal Tax*** NUM* 9
1723 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1724 '0', # 29 | Other Taxes & Fees*** NUM* 9
1733 time2str("%x", $self->_date),
1734 sprintf("%.2f", $self->charged),
1735 ( map { $cust_main->getfield($_) }
1736 qw( first last company address1 address2 city state zip country ) ),
1738 ) or die "can't create csv";
1741 my $header = $csv->string. "\n";
1744 if ( lc($opt{'format'}) eq 'billco' ) {
1747 foreach my $item ( $self->_items_pkg ) {
1750 '', # 1 | N/A-Leave Empty CHAR 2
1751 '', # 2 | N/A-Leave Empty CHAR 15
1752 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1753 $self->invnum, # 4 | Invoice Number CHAR 15
1754 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1755 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1756 $item->{'amount'}, # 7 | Amount NUM* 9
1757 '', # 8 | Line Format Control** CHAR 2
1758 '', # 9 | Grouping Code CHAR 2
1759 '', # 10 | User Defined CHAR 15
1762 $detail .= $csv->string. "\n";
1768 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1770 my($pkg, $setup, $recur, $sdate, $edate);
1771 if ( $cust_bill_pkg->pkgnum ) {
1773 ($pkg, $setup, $recur, $sdate, $edate) = (
1774 $cust_bill_pkg->part_pkg->pkg,
1775 ( $cust_bill_pkg->setup != 0
1776 ? sprintf("%.2f", $cust_bill_pkg->setup )
1778 ( $cust_bill_pkg->recur != 0
1779 ? sprintf("%.2f", $cust_bill_pkg->recur )
1781 ( $cust_bill_pkg->sdate
1782 ? time2str("%x", $cust_bill_pkg->sdate)
1784 ($cust_bill_pkg->edate
1785 ?time2str("%x", $cust_bill_pkg->edate)
1789 } else { #pkgnum tax
1790 next unless $cust_bill_pkg->setup != 0;
1791 $pkg = $cust_bill_pkg->desc;
1792 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1793 ( $sdate, $edate ) = ( '', '' );
1799 ( map { '' } (1..11) ),
1800 ($pkg, $setup, $recur, $sdate, $edate)
1801 ) or die "can't create csv";
1803 $detail .= $csv->string. "\n";
1809 ( $header, $detail );
1815 Pays this invoice with a compliemntary payment. If there is an error,
1816 returns the error, otherwise returns false.
1822 my $cust_pay = new FS::cust_pay ( {
1823 'invnum' => $self->invnum,
1824 'paid' => $self->owed,
1827 'payinfo' => $self->cust_main->payinfo,
1835 Attempts to pay this invoice with a credit card payment via a
1836 Business::OnlinePayment realtime gateway. See
1837 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1838 for supported processors.
1844 $self->realtime_bop( 'CC', @_ );
1849 Attempts to pay this invoice with an electronic check (ACH) payment via a
1850 Business::OnlinePayment realtime gateway. See
1851 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1852 for supported processors.
1858 $self->realtime_bop( 'ECHECK', @_ );
1863 Attempts to pay this invoice with phone bill (LEC) payment via a
1864 Business::OnlinePayment realtime gateway. See
1865 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1866 for supported processors.
1872 $self->realtime_bop( 'LEC', @_ );
1876 my( $self, $method ) = @_;
1878 my $cust_main = $self->cust_main;
1879 my $balance = $cust_main->balance;
1880 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1881 $amount = sprintf("%.2f", $amount);
1882 return "not run (balance $balance)" unless $amount > 0;
1884 my $description = 'Internet Services';
1885 if ( $conf->exists('business-onlinepayment-description') ) {
1886 my $dtempl = $conf->config('business-onlinepayment-description');
1888 my $agent_obj = $cust_main->agent
1889 or die "can't retreive agent for $cust_main (agentnum ".
1890 $cust_main->agentnum. ")";
1891 my $agent = $agent_obj->agent;
1892 my $pkgs = join(', ',
1893 map { $_->part_pkg->pkg }
1894 grep { $_->pkgnum } $self->cust_bill_pkg
1896 $description = eval qq("$dtempl");
1899 $cust_main->realtime_bop($method, $amount,
1900 'description' => $description,
1901 'invnum' => $self->invnum,
1906 =item batch_card OPTION => VALUE...
1908 Adds a payment for this invoice to the pending credit card batch (see
1909 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1910 runs the payment using a realtime gateway.
1915 my ($self, %options) = @_;
1916 my $cust_main = $self->cust_main;
1918 $options{invnum} = $self->invnum;
1920 $cust_main->batch_card(%options);
1923 sub _agent_template {
1925 $self->cust_main->agent_template;
1928 sub _agent_invoice_from {
1930 $self->cust_main->agent_invoice_from;
1933 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1935 Returns an text invoice, as a list of lines.
1937 Options can be passed as a hashref (recommended) or as a list of time, template
1938 and then any key/value pairs for any other options.
1940 I<time>, if specified, is used to control the printing of overdue messages. The
1941 default is now. It isn't the date of the invoice; that's the `_date' field.
1942 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1943 L<Time::Local> and L<Date::Parse> for conversion functions.
1945 I<template>, if specified, is the name of a suffix for alternate invoices.
1947 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1953 my( $today, $template, %opt );
1955 %opt = %{ shift() };
1956 $today = delete($opt{'time'}) || '';
1957 $template = delete($opt{template}) || '';
1959 ( $today, $template, %opt ) = @_;
1962 my %params = ( 'format' => 'template' );
1963 $params{'time'} = $today if $today;
1964 $params{'template'} = $template if $template;
1965 $params{$_} = $opt{$_}
1966 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1968 $self->print_generic( %params );
1971 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1973 Internal method - returns a filename of a filled-in LaTeX template for this
1974 invoice (Note: add ".tex" to get the actual filename), and a filename of
1975 an associated logo (with the .eps extension included).
1977 See print_ps and print_pdf for methods that return PostScript and PDF output.
1979 Options can be passed as a hashref (recommended) or as a list of time, template
1980 and then any key/value pairs for any other options.
1982 I<time>, if specified, is used to control the printing of overdue messages. The
1983 default is now. It isn't the date of the invoice; that's the `_date' field.
1984 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1985 L<Time::Local> and L<Date::Parse> for conversion functions.
1987 I<template>, if specified, is the name of a suffix for alternate invoices.
1989 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1995 my( $today, $template, %opt );
1997 %opt = %{ shift() };
1998 $today = delete($opt{'time'}) || '';
1999 $template = delete($opt{template}) || '';
2001 ( $today, $template, %opt ) = @_;
2004 my %params = ( 'format' => 'latex' );
2005 $params{'time'} = $today if $today;
2006 $params{'template'} = $template if $template;
2007 $params{$_} = $opt{$_}
2008 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2010 $template ||= $self->_agent_template;
2012 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2013 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2017 ) or die "can't open temp file: $!\n";
2019 my $agentnum = $self->cust_main->agentnum;
2021 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2022 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2023 or die "can't write temp file: $!\n";
2025 print $lh $conf->config_binary('logo.eps', $agentnum)
2026 or die "can't write temp file: $!\n";
2029 $params{'logo_file'} = $lh->filename;
2031 my @filled_in = $self->print_generic( %params );
2033 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2037 ) or die "can't open temp file: $!\n";
2038 print $fh join('', @filled_in );
2041 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2042 return ($1, $params{'logo_file'});
2046 =item print_generic OPTION => VALUE ...
2048 Internal method - returns a filled-in template for this invoice as a scalar.
2050 See print_ps and print_pdf for methods that return PostScript and PDF output.
2052 Non optional options include
2053 format - latex, html, template
2055 Optional options include
2057 template - a value used as a suffix for a configuration template
2059 time - a value used to control the printing of overdue messages. The
2060 default is now. It isn't the date of the invoice; that's the `_date' field.
2061 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2062 L<Time::Local> and L<Date::Parse> for conversion functions.
2066 unsquelch_cdr - overrides any per customer cdr squelching when true
2068 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2072 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2073 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2074 # yes: fixed width (dot matrix) text printing will be borked
2077 my( $self, %params ) = @_;
2078 my $today = $params{today} ? $params{today} : time;
2079 warn "$me print_generic called on $self with suffix $params{template}\n"
2082 my $format = $params{format};
2083 die "Unknown format: $format"
2084 unless $format =~ /^(latex|html|template)$/;
2086 my $cust_main = $self->cust_main;
2087 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2088 unless $cust_main->payname
2089 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2091 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2092 'html' => [ '<%=', '%>' ],
2093 'template' => [ '{', '}' ],
2096 #create the template
2097 my $template = $params{template} ? $params{template} : $self->_agent_template;
2098 my $templatefile = "invoice_$format";
2099 $templatefile .= "_$template"
2100 if length($template);
2101 my @invoice_template = map "$_\n", $conf->config($templatefile)
2102 or die "cannot load config data $templatefile";
2105 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2106 #change this to a die when the old code is removed
2107 warn "old-style invoice template $templatefile; ".
2108 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2109 $old_latex = 'true';
2110 @invoice_template = _translate_old_latex_format(@invoice_template);
2113 my $text_template = new Text::Template(
2115 SOURCE => \@invoice_template,
2116 DELIMITERS => $delimiters{$format},
2119 $text_template->compile()
2120 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2123 # additional substitution could possibly cause breakage in existing templates
2124 my %convert_maps = (
2126 'notes' => sub { map "$_", @_ },
2127 'footer' => sub { map "$_", @_ },
2128 'smallfooter' => sub { map "$_", @_ },
2129 'returnaddress' => sub { map "$_", @_ },
2130 'coupon' => sub { map "$_", @_ },
2131 'summary' => sub { map "$_", @_ },
2137 s/%%(.*)$/<!-- $1 -->/g;
2138 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2139 s/\\begin\{enumerate\}/<ol>/g;
2141 s/\\end\{enumerate\}/<\/ol>/g;
2142 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2151 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2153 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2158 s/\\\\\*?\s*$/<BR>/;
2159 s/\\hyphenation\{[\w\s\-]+}//;
2164 'coupon' => sub { "" },
2165 'summary' => sub { "" },
2172 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2173 s/\\begin\{enumerate\}//g;
2175 s/\\end\{enumerate\}//g;
2176 s/\\textbf\{(.*)\}/$1/g;
2183 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2185 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2190 s/\\\\\*?\s*$/\n/; # dubious
2191 s/\\hyphenation\{[\w\s\-]+}//;
2195 'coupon' => sub { "" },
2196 'summary' => sub { "" },
2201 # hashes for differing output formats
2202 my %nbsps = ( 'latex' => '~',
2203 'html' => '', # '&nbps;' would be nice
2204 'template' => '', # not used
2206 my $nbsp = $nbsps{$format};
2208 my %escape_functions = ( 'latex' => \&_latex_escape,
2209 'html' => \&encode_entities,
2210 'template' => sub { shift },
2212 my $escape_function = $escape_functions{$format};
2214 my %date_formats = ( 'latex' => '%b %o, %Y',
2215 'html' => '%b %o, %Y',
2218 my $date_format = $date_formats{$format};
2220 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2222 'html' => sub { return '<b>'. shift(). '</b>'
2224 'template' => sub { shift },
2226 my $embolden_function = $embolden_functions{$format};
2229 # generate template variables
2232 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2236 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2242 $returnaddress = join("\n",
2243 $conf->config_orbase("invoice_${format}returnaddress", $template)
2246 } elsif ( grep /\S/,
2247 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2249 my $convert_map = $convert_maps{$format}{'returnaddress'};
2252 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2257 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2259 my $convert_map = $convert_maps{$format}{'returnaddress'};
2260 $returnaddress = join( "\n", &$convert_map(
2261 map { s/( {2,})/'~' x length($1)/eg;
2265 ( $conf->config('company_name', $self->cust_main->agentnum),
2266 $conf->config('company_address', $self->cust_main->agentnum),
2273 my $warning = "Couldn't find a return address; ".
2274 "do you need to set the company_address configuration value?";
2276 $returnaddress = $nbsp;
2277 #$returnaddress = $warning;
2281 my $agentnum = $self->cust_main->agentnum;
2283 my %invoice_data = (
2286 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2287 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2288 'returnaddress' => $returnaddress,
2289 'agent' => &$escape_function($cust_main->agent->agent),
2292 'invnum' => $self->invnum,
2293 'date' => time2str($date_format, $self->_date),
2294 'today' => time2str('%b %o, %Y', $today),
2295 'terms' => $self->terms,
2296 'template' => $template, #params{'template'},
2297 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2298 'current_charges' => sprintf("%.2f", $self->charged),
2299 'duedate' => $self->due_date2str($rdate_format), #date_format?
2302 'custnum' => $cust_main->display_custnum,
2303 'agent_custid' => &$escape_function($cust_main->agent_custid),
2304 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2305 payname company address1 address2 city state zip fax
2309 'ship_enable' => $conf->exists('invoice-ship_address'),
2310 'unitprices' => $conf->exists('invoice-unitprice'),
2311 'smallernotes' => $conf->exists('invoice-smallernotes'),
2312 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2313 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2315 #layout info -- would be fancy to calc some of this and bury the template
2317 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2318 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2319 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2320 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2321 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2322 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2323 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2324 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2325 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2326 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2328 # better hang on to conf_dir for a while (for old templates)
2329 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2331 #these are only used when doing paged plaintext
2337 $invoice_data{finance_section} = '';
2338 if ( $conf->config('finance_pkgclass') ) {
2340 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2341 $invoice_data{finance_section} = $pkg_class->categoryname;
2343 $invoice_data{finance_amount} = '0.00';
2344 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2346 my $countrydefault = $conf->config('countrydefault') || 'US';
2347 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2348 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2349 my $method = $prefix.$_;
2350 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2352 $invoice_data{'ship_country'} = ''
2353 if ( $invoice_data{'ship_country'} eq $countrydefault );
2355 $invoice_data{'cid'} = $params{'cid'}
2358 if ( $cust_main->country eq $countrydefault ) {
2359 $invoice_data{'country'} = '';
2361 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2365 $invoice_data{'address'} = \@address;
2367 $cust_main->payname.
2368 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2369 ? " (P.O. #". $cust_main->payinfo. ")"
2373 push @address, $cust_main->company
2374 if $cust_main->company;
2375 push @address, $cust_main->address1;
2376 push @address, $cust_main->address2
2377 if $cust_main->address2;
2379 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2380 push @address, $invoice_data{'country'}
2381 if $invoice_data{'country'};
2383 while (scalar(@address) < 5);
2385 $invoice_data{'logo_file'} = $params{'logo_file'}
2386 if $params{'logo_file'};
2388 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2389 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2390 #my $balance_due = $self->owed + $pr_total - $cr_total;
2391 my $balance_due = $self->owed + $pr_total;
2392 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2393 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2394 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2395 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2397 my $summarypage = '';
2398 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2401 $invoice_data{'summarypage'} = $summarypage;
2403 #do variable substitution in notes, footer, smallfooter
2404 foreach my $include (qw( notes footer smallfooter coupon )) {
2406 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2409 if ( $conf->exists($inc_file, $agentnum)
2410 && length( $conf->config($inc_file, $agentnum) ) ) {
2412 @inc_src = $conf->config($inc_file, $agentnum);
2416 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2418 my $convert_map = $convert_maps{$format}{$include};
2420 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2421 s/--\@\]/$delimiters{$format}[1]/g;
2424 &$convert_map( $conf->config($inc_file, $agentnum) );
2428 my $inc_tt = new Text::Template (
2430 SOURCE => [ map "$_\n", @inc_src ],
2431 DELIMITERS => $delimiters{$format},
2432 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2434 unless ( $inc_tt->compile() ) {
2435 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2436 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2440 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2442 $invoice_data{$include} =~ s/\n+$//
2443 if ($format eq 'latex');
2446 $invoice_data{'po_line'} =
2447 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2448 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2451 my %money_chars = ( 'latex' => '',
2452 'html' => $conf->config('money_char') || '$',
2455 my $money_char = $money_chars{$format};
2457 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2458 'html' => $conf->config('money_char') || '$',
2461 my $other_money_char = $other_money_chars{$format};
2462 $invoice_data{'dollar'} = $other_money_char;
2464 my @detail_items = ();
2465 my @total_items = ();
2469 $invoice_data{'detail_items'} = \@detail_items;
2470 $invoice_data{'total_items'} = \@total_items;
2471 $invoice_data{'buf'} = \@buf;
2472 $invoice_data{'sections'} = \@sections;
2474 my $previous_section = { 'description' => 'Previous Charges',
2475 'subtotal' => $other_money_char.
2476 sprintf('%.2f', $pr_total),
2477 'summarized' => $summarypage ? 'Y' : '',
2479 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2480 join(' / ', map { $cust_main->balance_date_range(@$_) }
2481 $self->_prior_month30s
2483 if $conf->exists('invoice_include_aging');
2486 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2487 'subtotal' => $taxtotal, # adjusted below
2488 'summarized' => $summarypage ? 'Y' : '',
2490 my $tax_weight = _pkg_category($tax_section->{description})
2491 ? _pkg_category($tax_section->{description})->weight
2493 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2494 $tax_section->{'sort_weight'} = $tax_weight;
2497 my $adjusttotal = 0;
2498 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2499 'subtotal' => 0, # adjusted below
2500 'summarized' => $summarypage ? 'Y' : '',
2502 my $adjust_weight = _pkg_category($adjust_section->{description})
2503 ? _pkg_category($adjust_section->{description})->weight
2505 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2506 $adjust_section->{'sort_weight'} = $adjust_weight;
2508 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2509 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2510 $invoice_data{'multisection'} = $multisection;
2511 my $late_sections = [];
2512 my $extra_sections = [];
2513 my $extra_lines = ();
2514 if ( $multisection ) {
2515 ($extra_sections, $extra_lines) =
2516 $self->_items_extra_usage_sections($escape_function, $format)
2517 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2519 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2521 push @detail_items, @$extra_lines if $extra_lines;
2523 $self->_items_sections( $late_sections, # this could stand a refactor
2529 if ($conf->exists('svc_phone_sections')) {
2530 my ($phone_sections, $phone_lines) =
2531 $self->_items_svc_phone_sections($escape_function, $format);
2532 push @{$late_sections}, @$phone_sections;
2533 push @detail_items, @$phone_lines;
2536 push @sections, { 'description' => '', 'subtotal' => '' };
2539 unless ( $conf->exists('disable_previous_balance')
2540 || $conf->exists('previous_balance-summary_only')
2544 foreach my $line_item ( $self->_items_previous ) {
2547 ext_description => [],
2549 $detail->{'ref'} = $line_item->{'pkgnum'};
2550 $detail->{'quantity'} = 1;
2551 $detail->{'section'} = $previous_section;
2552 $detail->{'description'} = &$escape_function($line_item->{'description'});
2553 if ( exists $line_item->{'ext_description'} ) {
2554 @{$detail->{'ext_description'}} = map {
2555 &$escape_function($_);
2556 } @{$line_item->{'ext_description'}};
2558 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2559 $line_item->{'amount'};
2560 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2562 push @detail_items, $detail;
2563 push @buf, [ $detail->{'description'},
2564 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2570 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2571 push @buf, ['','-----------'];
2572 push @buf, [ 'Total Previous Balance',
2573 $money_char. sprintf("%10.2f", $pr_total) ];
2577 foreach my $section (@sections, @$late_sections) {
2579 # begin some normalization
2580 $section->{'subtotal'} = $section->{'amount'}
2582 && !exists($section->{subtotal})
2583 && exists($section->{amount});
2585 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2586 if ( $invoice_data{finance_section} &&
2587 $section->{'description'} eq $invoice_data{finance_section} );
2589 $section->{'subtotal'} = $other_money_char.
2590 sprintf('%.2f', $section->{'subtotal'})
2593 # continue some normalization
2594 $section->{'amount'} = $section->{'subtotal'}
2598 if ( $section->{'description'} ) {
2599 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2604 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2606 $options{'section'} = $section if $multisection;
2607 $options{'format'} = $format;
2608 $options{'escape_function'} = $escape_function;
2609 $options{'format_function'} = sub { () } unless $unsquelched;
2610 $options{'unsquelched'} = $unsquelched;
2611 $options{'summary_page'} = $summarypage;
2612 $options{'skip_usage'} =
2613 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2614 $options{'multilocation'} = $multilocation;
2615 $options{'multisection'} = $multisection;
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);
2998 unlink($file.'.tex');
3004 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3006 Returns an PDF invoice, as a scalar.
3008 Options can be passed as a hashref (recommended) or as a list of time, template
3009 and then any key/value pairs for any other options.
3011 I<time> an optional value used to control the printing of overdue messages. The
3012 default is now. It isn't the date of the invoice; that's the `_date' field.
3013 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3014 L<Time::Local> and L<Date::Parse> for conversion functions.
3016 I<template>, if specified, is the name of a suffix for alternate invoices.
3018 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3025 my ($file, $lfile) = $self->print_latex(@_);
3026 my $pdf = generate_pdf($file);
3027 unlink($file.'.tex');
3033 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3035 Returns an HTML invoice, as a scalar.
3037 I<time> an optional value used to control the printing of overdue messages. The
3038 default is now. It isn't the date of the invoice; that's the `_date' field.
3039 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3040 L<Time::Local> and L<Date::Parse> for conversion functions.
3042 I<template>, if specified, is the name of a suffix for alternate invoices.
3044 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3046 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3047 when emailing the invoice as part of a multipart/related MIME email.
3055 %params = %{ shift() };
3057 $params{'time'} = shift;
3058 $params{'template'} = shift;
3059 $params{'cid'} = shift;
3062 $params{'format'} = 'html';
3064 $self->print_generic( %params );
3067 # quick subroutine for print_latex
3069 # There are ten characters that LaTeX treats as special characters, which
3070 # means that they do not simply typeset themselves:
3071 # # $ % & ~ _ ^ \ { }
3073 # TeX ignores blanks following an escaped character; if you want a blank (as
3074 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3078 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3079 $value =~ s/([<>])/\$$1\$/g;
3083 #utility methods for print_*
3085 sub _translate_old_latex_format {
3086 warn "_translate_old_latex_format called\n"
3093 if ( $line =~ /^%%Detail\s*$/ ) {
3095 push @template, q![@--!,
3096 q! foreach my $_tr_line (@detail_items) {!,
3097 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3098 q! $_tr_line->{'description'} .= !,
3099 q! "\\tabularnewline\n~~".!,
3100 q! join( "\\tabularnewline\n~~",!,
3101 q! @{$_tr_line->{'ext_description'}}!,
3105 while ( ( my $line_item_line = shift )
3106 !~ /^%%EndDetail\s*$/ ) {
3107 $line_item_line =~ s/'/\\'/g; # nice LTS
3108 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3109 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3110 push @template, " \$OUT .= '$line_item_line';";
3113 push @template, '}',
3116 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3118 push @template, '[@--',
3119 ' foreach my $_tr_line (@total_items) {';
3121 while ( ( my $total_item_line = shift )
3122 !~ /^%%EndTotalDetails\s*$/ ) {
3123 $total_item_line =~ s/'/\\'/g; # nice LTS
3124 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3125 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3126 push @template, " \$OUT .= '$total_item_line';";
3129 push @template, '}',
3133 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3134 push @template, $line;
3140 warn "$_\n" foreach @template;
3149 #check for an invoice-specific override
3150 return $self->invoice_terms if $self->invoice_terms;
3152 #check for a customer- specific override
3153 my $cust_main = $self->cust_main;
3154 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3156 #use configured default
3157 $conf->config('invoice_default_terms') || '';
3163 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3164 $duedate = $self->_date() + ( $1 * 86400 );
3171 $self->due_date ? time2str(shift, $self->due_date) : '';
3174 sub balance_due_msg {
3176 my $msg = 'Balance Due';
3177 return $msg unless $self->terms;
3178 if ( $self->due_date ) {
3179 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3180 } elsif ( $self->terms ) {
3181 $msg .= ' - '. $self->terms;
3186 sub balance_due_date {
3189 if ( $conf->exists('invoice_default_terms')
3190 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3191 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3196 =item invnum_date_pretty
3198 Returns a string with the invoice number and date, for example:
3199 "Invoice #54 (3/20/2008)"
3203 sub invnum_date_pretty {
3205 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3210 Returns a string with the date, for example: "3/20/2008"
3216 time2str($date_format, $self->_date);
3219 use vars qw(%pkg_category_cache);
3220 sub _items_sections {
3223 my $summarypage = shift;
3225 my $extra_sections = shift;
3229 my %late_subtotal = ();
3232 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3235 my $usage = $cust_bill_pkg->usage;
3237 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3238 next if ( $display->summary && $summarypage );
3240 my $section = $display->section;
3241 my $type = $display->type;
3243 $not_tax{$section} = 1
3244 unless $cust_bill_pkg->pkgnum == 0;
3246 if ( $display->post_total && !$summarypage ) {
3247 if (! $type || $type eq 'S') {
3248 $late_subtotal{$section} += $cust_bill_pkg->setup
3249 if $cust_bill_pkg->setup != 0;
3253 $late_subtotal{$section} += $cust_bill_pkg->recur
3254 if $cust_bill_pkg->recur != 0;
3257 if ($type && $type eq 'R') {
3258 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3259 if $cust_bill_pkg->recur != 0;
3262 if ($type && $type eq 'U') {
3263 $late_subtotal{$section} += $usage
3264 unless scalar(@$extra_sections);
3269 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3271 if (! $type || $type eq 'S') {
3272 $subtotal{$section} += $cust_bill_pkg->setup
3273 if $cust_bill_pkg->setup != 0;
3277 $subtotal{$section} += $cust_bill_pkg->recur
3278 if $cust_bill_pkg->recur != 0;
3281 if ($type && $type eq 'R') {
3282 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3283 if $cust_bill_pkg->recur != 0;
3286 if ($type && $type eq 'U') {
3287 $subtotal{$section} += $usage
3288 unless scalar(@$extra_sections);
3297 %pkg_category_cache = ();
3299 push @$late, map { { 'description' => &{$escape}($_),
3300 'subtotal' => $late_subtotal{$_},
3302 'sort_weight' => ( _pkg_category($_)
3303 ? _pkg_category($_)->weight
3306 ((_pkg_category($_) && _pkg_category($_)->condense)
3307 ? $self->_condense_section($format)
3311 sort _sectionsort keys %late_subtotal;
3314 if ( $summarypage ) {
3315 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3316 map { $_->categoryname } qsearch('pkg_category', {});
3317 push @sections, '' if exists($subtotal{''});
3319 @sections = keys %subtotal;
3322 my @early = map { { 'description' => &{$escape}($_),
3323 'subtotal' => $subtotal{$_},
3324 'summarized' => $not_tax{$_} ? '' : 'Y',
3325 'tax_section' => $not_tax{$_} ? '' : 'Y',
3326 'sort_weight' => ( _pkg_category($_)
3327 ? _pkg_category($_)->weight
3330 ((_pkg_category($_) && _pkg_category($_)->condense)
3331 ? $self->_condense_section($format)
3336 push @early, @$extra_sections if $extra_sections;
3338 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3342 #helper subs for above
3345 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3349 my $categoryname = shift;
3350 $pkg_category_cache{$categoryname} ||=
3351 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3354 my %condensed_format = (
3355 'label' => [ qw( Description Qty Amount ) ],
3357 sub { shift->{description} },
3358 sub { shift->{quantity} },
3359 sub { my($href, %opt) = @_;
3360 ($opt{dollar} || ''). $href->{amount};
3363 'align' => [ qw( l r r ) ],
3364 'span' => [ qw( 5 1 1 ) ], # unitprices?
3365 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3368 sub _condense_section {
3369 my ( $self, $format ) = ( shift, shift );
3371 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3372 qw( description_generator
3375 total_line_generator
3380 sub _condensed_generator_defaults {
3381 my ( $self, $format ) = ( shift, shift );
3382 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3391 sub _condensed_header_generator {
3392 my ( $self, $format ) = ( shift, shift );
3394 my ( $f, $prefix, $suffix, $separator, $column ) =
3395 _condensed_generator_defaults($format);
3397 if ($format eq 'latex') {
3398 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3399 $suffix = "\\\\\n\\hline";
3402 sub { my ($d,$a,$s,$w) = @_;
3403 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3405 } elsif ( $format eq 'html' ) {
3406 $prefix = '<th></th>';
3410 sub { my ($d,$a,$s,$w) = @_;
3411 return qq!<th align="$html_align{$a}">$d</th>!;
3419 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3421 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3424 $prefix. join($separator, @result). $suffix;
3429 sub _condensed_description_generator {
3430 my ( $self, $format ) = ( shift, shift );
3432 my ( $f, $prefix, $suffix, $separator, $column ) =
3433 _condensed_generator_defaults($format);
3435 my $money_char = '$';
3436 if ($format eq 'latex') {
3437 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3439 $separator = " & \n";
3441 sub { my ($d,$a,$s,$w) = @_;
3442 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3444 $money_char = '\\dollar';
3445 }elsif ( $format eq 'html' ) {
3446 $prefix = '"><td align="center"></td>';
3450 sub { my ($d,$a,$s,$w) = @_;
3451 return qq!<td align="$html_align{$a}">$d</td>!;
3453 #$money_char = $conf->config('money_char') || '$';
3454 $money_char = ''; # this is madness
3462 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3464 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3466 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3467 map { $f->{$_}->[$i] } qw(align span width)
3471 $prefix. join( $separator, @result ). $suffix;
3476 sub _condensed_total_generator {
3477 my ( $self, $format ) = ( shift, shift );
3479 my ( $f, $prefix, $suffix, $separator, $column ) =
3480 _condensed_generator_defaults($format);
3483 if ($format eq 'latex') {
3486 $separator = " & \n";
3488 sub { my ($d,$a,$s,$w) = @_;
3489 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3491 }elsif ( $format eq 'html' ) {
3495 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3497 sub { my ($d,$a,$s,$w) = @_;
3498 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3507 # my $r = &{$f->{fields}->[$i]}(@args);
3508 # $r .= ' Total' unless $i;
3510 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3512 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3513 map { $f->{$_}->[$i] } qw(align span width)
3517 $prefix. join( $separator, @result ). $suffix;
3522 =item total_line_generator FORMAT
3524 Returns a coderef used for generation of invoice total line items for this
3525 usage_class. FORMAT is either html or latex
3529 # should not be used: will have issues with hash element names (description vs
3530 # total_item and amount vs total_amount -- another array of functions?
3532 sub _condensed_total_line_generator {
3533 my ( $self, $format ) = ( shift, shift );
3535 my ( $f, $prefix, $suffix, $separator, $column ) =
3536 _condensed_generator_defaults($format);
3539 if ($format eq 'latex') {
3542 $separator = " & \n";
3544 sub { my ($d,$a,$s,$w) = @_;
3545 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3547 }elsif ( $format eq 'html' ) {
3551 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3553 sub { my ($d,$a,$s,$w) = @_;
3554 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3563 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3565 &{$column}( &{$f->{fields}->[$i]}(@args),
3566 map { $f->{$_}->[$i] } qw(align span width)
3570 $prefix. join( $separator, @result ). $suffix;
3575 #sub _items_extra_usage_sections {
3577 # my $escape = shift;
3579 # my %sections = ();
3581 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3582 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3584 # next unless $cust_bill_pkg->pkgnum > 0;
3586 # foreach my $section ( keys %usage_class ) {
3588 # my $usage = $cust_bill_pkg->usage($section);
3590 # next unless $usage && $usage > 0;
3592 # $sections{$section} ||= 0;
3593 # $sections{$section} += $usage;
3599 # map { { 'description' => &{$escape}($_),
3600 # 'subtotal' => $sections{$_},
3601 # 'summarized' => '',
3602 # 'tax_section' => '',
3605 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3609 sub _items_extra_usage_sections {
3618 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3619 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3620 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3621 next unless $cust_bill_pkg->pkgnum > 0;
3623 foreach my $classnum ( keys %usage_class ) {
3624 my $section = $usage_class{$classnum}->classname;
3625 $classnums{$section} = $classnum;
3627 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3628 my $amount = $detail->amount;
3629 next unless $amount && $amount > 0;
3631 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3632 $sections{$section}{amount} += $amount; #subtotal
3633 $sections{$section}{calls}++;
3634 $sections{$section}{duration} += $detail->duration;
3636 my $desc = $detail->regionname;
3637 my $description = $desc;
3638 $description = substr($desc, 0, 50). '...'
3639 if $format eq 'latex' && length($desc) > 50;
3641 $lines{$section}{$desc} ||= {
3642 description => &{$escape}($description),
3643 #pkgpart => $part_pkg->pkgpart,
3644 pkgnum => $cust_bill_pkg->pkgnum,
3649 #unit_amount => $cust_bill_pkg->unitrecur,
3650 quantity => $cust_bill_pkg->quantity,
3651 product_code => 'N/A',
3652 ext_description => [],
3655 $lines{$section}{$desc}{amount} += $amount;
3656 $lines{$section}{$desc}{calls}++;
3657 $lines{$section}{$desc}{duration} += $detail->duration;
3663 my %sectionmap = ();
3664 foreach (keys %sections) {
3665 my $usage_class = $usage_class{$classnums{$_}};
3666 $sectionmap{$_} = { 'description' => &{$escape}($_),
3667 'amount' => $sections{$_}{amount}, #subtotal
3668 'calls' => $sections{$_}{calls},
3669 'duration' => $sections{$_}{duration},
3671 'tax_section' => '',
3672 'sort_weight' => $usage_class->weight,
3673 ( $usage_class->format
3674 ? ( map { $_ => $usage_class->$_($format) }
3675 qw( description_generator header_generator total_generator total_line_generator )
3682 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3686 foreach my $section ( keys %lines ) {
3687 foreach my $line ( keys %{$lines{$section}} ) {
3688 my $l = $lines{$section}{$line};
3689 $l->{section} = $sectionmap{$section};
3690 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3691 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3696 return(\@sections, \@lines);
3700 sub _items_svc_phone_sections {
3709 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3711 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3712 next unless $cust_bill_pkg->pkgnum > 0;
3714 my @header = $cust_bill_pkg->details_header;
3715 next unless scalar(@header);
3717 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3719 my $phonenum = $detail->phonenum;
3720 next unless $phonenum;
3722 my $amount = $detail->amount;
3723 next unless $amount && $amount > 0;
3725 $sections{$phonenum} ||= { 'amount' => 0,
3728 'sort_weight' => -1,
3729 'phonenum' => $phonenum,
3731 $sections{$phonenum}{amount} += $amount; #subtotal
3732 $sections{$phonenum}{calls}++;
3733 $sections{$phonenum}{duration} += $detail->duration;
3735 my $desc = $detail->regionname;
3736 my $description = $desc;
3737 $description = substr($desc, 0, 50). '...'
3738 if $format eq 'latex' && length($desc) > 50;
3740 $lines{$phonenum}{$desc} ||= {
3741 description => &{$escape}($description),
3742 #pkgpart => $part_pkg->pkgpart,
3750 product_code => 'N/A',
3751 ext_description => [],
3754 $lines{$phonenum}{$desc}{amount} += $amount;
3755 $lines{$phonenum}{$desc}{calls}++;
3756 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3758 my $line = $usage_class{$detail->classnum}->classname;
3759 $sections{"$phonenum $line"} ||=
3763 'sort_weight' => $usage_class{$detail->classnum}->weight,
3764 'phonenum' => $phonenum,
3765 'header' => [ @header ],
3767 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3768 $sections{"$phonenum $line"}{calls}++;
3769 $sections{"$phonenum $line"}{duration} += $detail->duration;
3771 $lines{"$phonenum $line"}{$desc} ||= {
3772 description => &{$escape}($description),
3773 #pkgpart => $part_pkg->pkgpart,
3781 product_code => 'N/A',
3782 ext_description => [],
3785 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3786 $lines{"$phonenum $line"}{$desc}{calls}++;
3787 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3788 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3789 $detail->formatted('format' => $format);
3794 my %sectionmap = ();
3795 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3796 foreach ( keys %sections ) {
3797 my @header = @{ $sections{$_}{header} || [] };
3799 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
3800 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3801 my $usage_class = $summary ? $simple : $usage_simple;
3802 my $ending = $summary ? ' usage charges' : '';
3805 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
3807 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3808 'amount' => $sections{$_}{amount}, #subtotal
3809 'calls' => $sections{$_}{calls},
3810 'duration' => $sections{$_}{duration},
3812 'tax_section' => '',
3813 'phonenum' => $sections{$_}{phonenum},
3814 'sort_weight' => $sections{$_}{sort_weight},
3815 'post_total' => $summary, #inspire pagebreak
3817 ( map { $_ => $usage_class->$_($format, %gen_opt) }
3818 qw( description_generator
3821 total_line_generator
3828 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3829 $a->{sort_weight} <=> $b->{sort_weight}
3834 foreach my $section ( keys %lines ) {
3835 foreach my $line ( keys %{$lines{$section}} ) {
3836 my $l = $lines{$section}{$line};
3837 $l->{section} = $sectionmap{$section};
3838 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3839 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3844 return(\@sections, \@lines);
3851 #my @display = scalar(@_)
3853 # : qw( _items_previous _items_pkg );
3854 # #: qw( _items_pkg );
3855 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3856 my @display = qw( _items_previous _items_pkg );
3859 foreach my $display ( @display ) {
3860 push @b, $self->$display(@_);
3865 sub _items_previous {
3867 my $cust_main = $self->cust_main;
3868 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3870 foreach ( @pr_cust_bill ) {
3871 my $date = $conf->exists('invoice_show_prior_due_date')
3872 ? 'due '. $_->due_date2str($date_format)
3873 : time2str($date_format, $_->_date);
3875 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3876 #'pkgpart' => 'N/A',
3878 'amount' => sprintf("%.2f", $_->owed),
3884 # 'description' => 'Previous Balance',
3885 # #'pkgpart' => 'N/A',
3886 # 'pkgnum' => 'N/A',
3887 # 'amount' => sprintf("%10.2f", $pr_total ),
3888 # 'ext_description' => [ map {
3889 # "Invoice ". $_->invnum.
3890 # " (". time2str("%x",$_->_date). ") ".
3891 # sprintf("%10.2f", $_->owed)
3892 # } @pr_cust_bill ],
3900 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3901 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3902 if ($options{section} && $options{section}->{condensed}) {
3904 local $Storable::canonical = 1;
3905 foreach ( @items ) {
3907 delete $item->{ref};
3908 delete $item->{ext_description};
3909 my $key = freeze($item);
3910 $itemshash{$key} ||= 0;
3911 $itemshash{$key} ++; # += $item->{quantity};
3913 @items = sort { $a->{description} cmp $b->{description} }
3914 map { my $i = thaw($_);
3915 $i->{quantity} = $itemshash{$_};
3917 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3926 return 0 unless $a->itemdesc cmp $b->itemdesc;
3927 return -1 if $b->itemdesc eq 'Tax';
3928 return 1 if $a->itemdesc eq 'Tax';
3929 return -1 if $b->itemdesc eq 'Other surcharges';
3930 return 1 if $a->itemdesc eq 'Other surcharges';
3931 $a->itemdesc cmp $b->itemdesc;
3936 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3937 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3940 sub _items_cust_bill_pkg {
3942 my $cust_bill_pkg = shift;
3945 my $format = $opt{format} || '';
3946 my $escape_function = $opt{escape_function} || sub { shift };
3947 my $format_function = $opt{format_function} || '';
3948 my $unsquelched = $opt{unsquelched} || '';
3949 my $section = $opt{section}->{description} if $opt{section};
3950 my $summary_page = $opt{summary_page} || '';
3951 my $multilocation = $opt{multilocation} || '';
3952 my $multisection = $opt{multisection} || '';
3955 my ($s, $r, $u) = ( undef, undef, undef );
3956 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3959 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3960 if ( $_ && !$cust_bill_pkg->hidden ) {
3961 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
3962 $_->{amount} =~ s/^\-0\.00$/0.00/;
3963 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3965 unless $_->{amount} == 0;
3970 foreach my $display ( grep { defined($section)
3971 ? $_->section eq $section
3974 #grep { !$_->summary || !$summary_page } # bunk!
3975 grep { !$_->summary || $multisection }
3976 $cust_bill_pkg->cust_bill_pkg_display
3980 my $type = $display->type;
3982 my $desc = $cust_bill_pkg->desc;
3983 $desc = substr($desc, 0, 50). '...'
3984 if $format eq 'latex' && length($desc) > 50;
3986 my %details_opt = ( 'format' => $format,
3987 'escape_function' => $escape_function,
3988 'format_function' => $format_function,
3991 if ( $cust_bill_pkg->pkgnum > 0 ) {
3993 my $cust_pkg = $cust_bill_pkg->cust_pkg;
3995 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3997 my $description = $desc;
3998 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4001 unless ( $cust_pkg->part_pkg->hide_svc_detail
4002 || $cust_bill_pkg->hidden )
4004 push @d, map &{$escape_function}($_),
4005 $cust_pkg->h_labels_short($self->_date);
4006 if ( $multilocation ) {
4007 my $loc = $cust_pkg->location_label;
4008 $loc = substr($desc, 0, 50). '...'
4009 if $format eq 'latex' && length($loc) > 50;
4010 push @d, &{$escape_function}($loc);
4013 push @d, $cust_bill_pkg->details(%details_opt)
4014 if $cust_bill_pkg->recur == 0;
4016 if ( $cust_bill_pkg->hidden ) {
4017 $s->{amount} += $cust_bill_pkg->setup;
4018 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4019 push @{ $s->{ext_description} }, @d;
4022 description => $description,
4023 #pkgpart => $part_pkg->pkgpart,
4024 pkgnum => $cust_bill_pkg->pkgnum,
4025 amount => $cust_bill_pkg->setup,
4026 unit_amount => $cust_bill_pkg->unitsetup,
4027 quantity => $cust_bill_pkg->quantity,
4028 ext_description => \@d,
4034 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ) &&
4035 ( !$type || $type eq 'R' || $type eq 'U' )
4039 my $is_summary = $display->summary;
4040 my $description = ($is_summary && $type && $type eq 'U')
4041 ? "Usage charges" : $desc;
4043 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4044 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4045 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4050 #at least until cust_bill_pkg has "past" ranges in addition to
4051 #the "future" sdate/edate ones... see #3032
4052 my @dates = ( $self->_date );
4053 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4054 push @dates, $prev->sdate if $prev;
4056 unless ( $cust_pkg->part_pkg->hide_svc_detail
4057 || $cust_bill_pkg->itemdesc
4058 || $cust_bill_pkg->hidden
4059 || $is_summary && $type && $type eq 'U' )
4061 push @d, map &{$escape_function}($_),
4062 $cust_pkg->h_labels_short(@dates)
4063 #$cust_bill_pkg->edate,
4064 #$cust_bill_pkg->sdate)
4066 if ( $multilocation ) {
4067 my $loc = $cust_pkg->location_label;
4068 $loc = substr($desc, 0, 50). '...'
4069 if $format eq 'latex' && length($loc) > 50;
4070 push @d, &{$escape_function}($loc);
4074 push @d, $cust_bill_pkg->details(%details_opt)
4075 unless ($is_summary || $type && $type eq 'R');
4079 $amount = $cust_bill_pkg->recur;
4080 }elsif($type eq 'R') {
4081 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4082 }elsif($type eq 'U') {
4083 $amount = $cust_bill_pkg->usage;
4086 if ( !$type || $type eq 'R' ) {
4088 if ( $cust_bill_pkg->hidden ) {
4089 $r->{amount} += $amount;
4090 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4091 push @{ $r->{ext_description} }, @d;
4094 description => $description,
4095 #pkgpart => $part_pkg->pkgpart,
4096 pkgnum => $cust_bill_pkg->pkgnum,
4098 unit_amount => $cust_bill_pkg->unitrecur,
4099 quantity => $cust_bill_pkg->quantity,
4100 ext_description => \@d,
4104 } else { # $type eq 'U'
4106 if ( $cust_bill_pkg->hidden ) {
4107 $u->{amount} += $amount;
4108 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4109 push @{ $u->{ext_description} }, @d;
4112 description => $description,
4113 #pkgpart => $part_pkg->pkgpart,
4114 pkgnum => $cust_bill_pkg->pkgnum,
4116 unit_amount => $cust_bill_pkg->unitrecur,
4117 quantity => $cust_bill_pkg->quantity,
4118 ext_description => \@d,
4124 } # recurring or usage with recurring charge
4126 } else { #pkgnum tax or one-shot line item (??)
4128 if ( $cust_bill_pkg->setup != 0 ) {
4130 'description' => $desc,
4131 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4134 if ( $cust_bill_pkg->recur != 0 ) {
4136 'description' => "$desc (".
4137 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4138 time2str($date_format, $cust_bill_pkg->edate). ')',
4139 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4149 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4151 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4152 $_->{amount} =~ s/^\-0\.00$/0.00/;
4153 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4155 unless $_->{amount} == 0;
4163 sub _items_credits {
4164 my( $self, %opt ) = @_;
4165 my $trim_len = $opt{'trim_len'} || 60;
4169 foreach ( $self->cust_credited ) {
4171 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4173 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4174 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4175 $reason = " ($reason) " if $reason;
4178 #'description' => 'Credit ref\#'. $_->crednum.
4179 # " (". time2str("%x",$_->cust_credit->_date) .")".
4181 'description' => 'Credit applied '.
4182 time2str($date_format,$_->cust_credit->_date). $reason,
4183 'amount' => sprintf("%.2f",$_->amount),
4191 sub _items_payments {
4195 #get & print payments
4196 foreach ( $self->cust_bill_pay ) {
4198 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4201 'description' => "Payment received ".
4202 time2str($date_format,$_->cust_pay->_date ),
4203 'amount' => sprintf("%.2f", $_->amount )
4211 =item call_details [ OPTION => VALUE ... ]
4213 Returns an array of CSV strings representing the call details for this invoice
4214 The only option available is the boolean prepend_billed_number
4219 my ($self, %opt) = @_;
4221 my $format_function = sub { shift };
4223 if ($opt{prepend_billed_number}) {
4224 $format_function = sub {
4228 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4233 my @details = map { $_->details( 'format_function' => $format_function,
4234 'escape_function' => sub{ return() },
4238 $self->cust_bill_pkg;
4239 my $header = $details[0];
4240 ( $header, grep { $_ ne $header } @details );
4250 =item process_reprint
4254 sub process_reprint {
4255 process_re_X('print', @_);
4258 =item process_reemail
4262 sub process_reemail {
4263 process_re_X('email', @_);
4271 process_re_X('fax', @_);
4279 process_re_X('ftp', @_);
4286 sub process_respool {
4287 process_re_X('spool', @_);
4290 use Storable qw(thaw);
4294 my( $method, $job ) = ( shift, shift );
4295 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4297 my $param = thaw(decode_base64(shift));
4298 warn Dumper($param) if $DEBUG;
4309 my($method, $job, %param ) = @_;
4311 warn "re_X $method for job $job with param:\n".
4312 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4315 #some false laziness w/search/cust_bill.html
4317 my $orderby = 'ORDER BY cust_bill._date';
4319 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4321 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4323 my @cust_bill = qsearch( {
4324 #'select' => "cust_bill.*",
4325 'table' => 'cust_bill',
4326 'addl_from' => $addl_from,
4328 'extra_sql' => $extra_sql,
4329 'order_by' => $orderby,
4333 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4335 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4338 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4339 foreach my $cust_bill ( @cust_bill ) {
4340 $cust_bill->$method();
4342 if ( $job ) { #progressbar foo
4344 if ( time - $min_sec > $last ) {
4345 my $error = $job->update_statustext(
4346 int( 100 * $num / scalar(@cust_bill) )
4348 die $error if $error;
4359 =head1 CLASS METHODS
4365 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4370 my ($class, $start, $end) = @_;
4372 $class->paid_sql($start, $end). ' - '.
4373 $class->credited_sql($start, $end);
4378 Returns an SQL fragment to retreive the net amount (charged minus credited).
4383 my ($class, $start, $end) = @_;
4384 'charged - '. $class->credited_sql($start, $end);
4389 Returns an SQL fragment to retreive the amount paid against this invoice.
4394 my ($class, $start, $end) = @_;
4395 $start &&= "AND cust_bill_pay._date <= $start";
4396 $end &&= "AND cust_bill_pay._date > $end";
4397 $start = '' unless defined($start);
4398 $end = '' unless defined($end);
4399 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4400 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4405 Returns an SQL fragment to retreive the amount credited against this invoice.
4410 my ($class, $start, $end) = @_;
4411 $start &&= "AND cust_credit_bill._date <= $start";
4412 $end &&= "AND cust_credit_bill._date > $end";
4413 $start = '' unless defined($start);
4414 $end = '' unless defined($end);
4415 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4416 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4419 =item search_sql_where HASHREF
4421 Class method which returns an SQL WHERE fragment to search for parameters
4422 specified in HASHREF. Valid parameters are
4428 List reference of start date, end date, as UNIX timestamps.
4438 List reference of charged limits (exclusive).
4442 List reference of charged limits (exclusive).
4446 flag, return open invoices only
4450 flag, return net invoices only
4454 =item newest_percust
4458 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4462 sub search_sql_where {
4463 my($class, $param) = @_;
4465 warn "$me search_sql_where called with params: \n".
4466 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4472 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4473 push @search, "cust_main.agentnum = $1";
4477 if ( $param->{_date} ) {
4478 my($beginning, $ending) = @{$param->{_date}};
4480 push @search, "cust_bill._date >= $beginning",
4481 "cust_bill._date < $ending";
4485 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4486 push @search, "cust_bill.invnum >= $1";
4488 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4489 push @search, "cust_bill.invnum <= $1";
4493 if ( $param->{charged} ) {
4494 my @charged = ref($param->{charged})
4495 ? @{ $param->{charged} }
4496 : ($param->{charged});
4498 push @search, map { s/^charged/cust_bill.charged/; $_; }
4502 my $owed_sql = FS::cust_bill->owed_sql;
4505 if ( $param->{owed} ) {
4506 my @owed = ref($param->{owed})
4507 ? @{ $param->{owed} }
4509 push @search, map { s/^owed/$owed_sql/; $_; }
4514 push @search, "0 != $owed_sql"
4515 if $param->{'open'};
4516 push @search, '0 != '. FS::cust_bill->net_sql
4520 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4521 if $param->{'days'};
4524 if ( $param->{'newest_percust'} ) {
4526 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4527 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4529 my @newest_where = map { my $x = $_;
4530 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4533 grep ! /^cust_main./, @search;
4534 my $newest_where = scalar(@newest_where)
4535 ? ' AND '. join(' AND ', @newest_where)
4539 push @search, "cust_bill._date = (
4540 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4541 WHERE newest_cust_bill.custnum = cust_bill.custnum
4547 #agent virtualization
4548 my $curuser = $FS::CurrentUser::CurrentUser;
4549 if ( $curuser->username eq 'fs_queue'
4550 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4552 my $newuser = qsearchs('access_user', {
4553 'username' => $username,
4557 $curuser = $newuser;
4559 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4562 push @search, $curuser->agentnums_sql;
4564 join(' AND ', @search );
4576 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4577 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base