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 use FS::cust_bill_batch;
40 @ISA = qw( FS::cust_main_Mixin FS::Record );
43 $me = '[FS::cust_bill]';
45 #ask FS::UID to run this stuff for us later
46 FS::UID->install_callback( sub {
48 $money_char = $conf->config('money_char') || '$';
49 $date_format = $conf->config('date_format') || '%x';
50 $rdate_format = $conf->config('date_format') || '%m/%d/%Y';
55 FS::cust_bill - Object methods for cust_bill records
61 $record = new FS::cust_bill \%hash;
62 $record = new FS::cust_bill { 'column' => 'value' };
64 $error = $record->insert;
66 $error = $new_record->replace($old_record);
68 $error = $record->delete;
70 $error = $record->check;
72 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
74 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
76 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
78 @cust_pay_objects = $cust_bill->cust_pay;
80 $tax_amount = $record->tax;
82 @lines = $cust_bill->print_text;
83 @lines = $cust_bill->print_text $time;
87 An FS::cust_bill object represents an invoice; a declaration that a customer
88 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
89 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
90 following fields are currently supported:
96 =item invnum - primary key (assigned automatically for new invoices)
98 =item custnum - customer (see L<FS::cust_main>)
100 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
101 L<Time::Local> and L<Date::Parse> for conversion functions.
103 =item charged - amount of this invoice
105 =item invoice_terms - optional terms override for this specific invoice
109 Customer info at invoice generation time
113 =item previous_balance
115 =item billing_balance
123 =item printed - deprecated
131 =item closed - books closed flag, empty or `Y'
133 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
135 =item agent_invid - legacy invoice number
145 Creates a new invoice. To add the invoice to the database, see L<"insert">.
146 Invoices are normally created by calling the bill method of a customer object
147 (see L<FS::cust_main>).
151 sub table { 'cust_bill'; }
153 sub cust_linked { $_[0]->cust_main_custnum; }
154 sub cust_unlinked_msg {
156 "WARNING: can't find cust_main.custnum ". $self->custnum.
157 ' (cust_bill.invnum '. $self->invnum. ')';
162 Adds this invoice to the database ("Posts" the invoice). If there is an error,
163 returns the error, otherwise returns false.
167 This method now works but you probably shouldn't use it. Instead, apply a
168 credit against the invoice.
170 Using this method to delete invoices outright is really, really bad. There
171 would be no record you ever posted this invoice, and there are no check to
172 make sure charged = 0 or that there are no associated cust_bill_pkg records.
174 Really, don't use it.
180 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
182 local $SIG{HUP} = 'IGNORE';
183 local $SIG{INT} = 'IGNORE';
184 local $SIG{QUIT} = 'IGNORE';
185 local $SIG{TERM} = 'IGNORE';
186 local $SIG{TSTP} = 'IGNORE';
187 local $SIG{PIPE} = 'IGNORE';
189 my $oldAutoCommit = $FS::UID::AutoCommit;
190 local $FS::UID::AutoCommit = 0;
193 foreach my $table (qw(
205 foreach my $linked ( $self->$table() ) {
206 my $error = $linked->delete;
208 $dbh->rollback if $oldAutoCommit;
215 my $error = $self->SUPER::delete(@_);
217 $dbh->rollback if $oldAutoCommit;
221 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
227 =item replace OLD_RECORD
229 Replaces the OLD_RECORD with this one in the database. If there is an error,
230 returns the error, otherwise returns false.
232 Only printed may be changed. printed is normally updated by calling the
233 collect method of a customer object (see L<FS::cust_main>).
237 #replace can be inherited from Record.pm
239 # replace_check is now the preferred way to #implement replace data checks
240 # (so $object->replace() works without an argument)
243 my( $new, $old ) = ( shift, shift );
244 return "Can't change custnum!" unless $old->custnum == $new->custnum;
245 #return "Can't change _date!" unless $old->_date eq $new->_date;
246 return "Can't change _date!" unless $old->_date == $new->_date;
247 return "Can't change charged!" unless $old->charged == $new->charged
248 || $old->charged == 0;
255 Checks all fields to make sure this is a valid invoice. If there is an error,
256 returns the error, otherwise returns false. Called by the insert and replace
265 $self->ut_numbern('invnum')
266 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
267 || $self->ut_numbern('_date')
268 || $self->ut_money('charged')
269 || $self->ut_numbern('printed')
270 || $self->ut_enum('closed', [ '', 'Y' ])
271 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
272 || $self->ut_numbern('agent_invid') #varchar?
274 return $error if $error;
276 $self->_date(time) unless $self->_date;
278 $self->printed(0) if $self->printed eq '';
285 Returns the displayed invoice number for this invoice: agent_invid if
286 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
292 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
293 return $self->agent_invid;
295 return $self->invnum;
301 Returns a list consisting of the total previous balance for this customer,
302 followed by the previous outstanding invoices (as FS::cust_bill objects also).
309 my @cust_bill = sort { $a->_date <=> $b->_date }
310 grep { $_->owed != 0 && $_->_date < $self->_date }
311 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
313 foreach ( @cust_bill ) { $total += $_->owed; }
319 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
326 { 'table' => 'cust_bill_pkg',
327 'hashref' => { 'invnum' => $self->invnum },
328 'order_by' => 'ORDER BY billpkgnum',
333 =item cust_bill_pkg_pkgnum PKGNUM
335 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
340 sub cust_bill_pkg_pkgnum {
341 my( $self, $pkgnum ) = @_;
343 { 'table' => 'cust_bill_pkg',
344 'hashref' => { 'invnum' => $self->invnum,
347 'order_by' => 'ORDER BY billpkgnum',
354 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
361 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
362 $self->cust_bill_pkg;
364 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
369 Returns true if any of the packages (or their definitions) corresponding to the
370 line items for this invoice have the no_auto flag set.
376 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
379 =item open_cust_bill_pkg
381 Returns the open line items for this invoice.
383 Note that cust_bill_pkg with both setup and recur fees are returned as two
384 separate line items, each with only one fee.
388 # modeled after cust_main::open_cust_bill
389 sub open_cust_bill_pkg {
392 # grep { $_->owed > 0 } $self->cust_bill_pkg
394 my %other = ( 'recur' => 'setup',
395 'setup' => 'recur', );
397 foreach my $field ( qw( recur setup )) {
398 push @open, map { $_->set( $other{$field}, 0 ); $_; }
399 grep { $_->owed($field) > 0 }
400 $self->cust_bill_pkg;
406 =item cust_bill_event
408 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
412 sub cust_bill_event {
414 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
417 =item num_cust_bill_event
419 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
423 sub num_cust_bill_event {
426 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
427 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
428 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
429 $sth->fetchrow_arrayref->[0];
434 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
438 #false laziness w/cust_pkg.pm
442 'table' => 'cust_event',
443 'addl_from' => 'JOIN part_event USING ( eventpart )',
444 'hashref' => { 'tablenum' => $self->invnum },
445 'extra_sql' => " AND eventtable = 'cust_bill' ",
451 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
455 #false laziness w/cust_pkg.pm
459 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
460 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
461 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
462 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
463 $sth->fetchrow_arrayref->[0];
468 Returns the customer (see L<FS::cust_main>) for this invoice.
474 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
477 =item cust_suspend_if_balance_over AMOUNT
479 Suspends the customer associated with this invoice if the total amount owed on
480 this invoice and all older invoices is greater than the specified amount.
482 Returns a list: an empty list on success or a list of errors.
486 sub cust_suspend_if_balance_over {
487 my( $self, $amount ) = ( shift, shift );
488 my $cust_main = $self->cust_main;
489 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
492 $cust_main->suspend(@_);
498 Depreciated. See the cust_credited method.
500 #Returns a list consisting of the total previous credited (see
501 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
502 #outstanding credits (FS::cust_credit objects).
508 croak "FS::cust_bill->cust_credit depreciated; see ".
509 "FS::cust_bill->cust_credit_bill";
512 #my @cust_credit = sort { $a->_date <=> $b->_date }
513 # grep { $_->credited != 0 && $_->_date < $self->_date }
514 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
516 #foreach (@cust_credit) { $total += $_->credited; }
517 #$total, @cust_credit;
522 Depreciated. See the cust_bill_pay method.
524 #Returns all payments (see L<FS::cust_pay>) for this invoice.
530 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
532 #sort { $a->_date <=> $b->_date }
533 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
539 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
542 sub cust_bill_pay_batch {
544 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
549 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
555 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
556 sort { $a->_date <=> $b->_date }
557 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
562 =item cust_credit_bill
564 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
570 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
571 sort { $a->_date <=> $b->_date }
572 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
576 sub cust_credit_bill {
577 shift->cust_credited(@_);
580 =item cust_bill_pay_pkgnum PKGNUM
582 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
583 with matching pkgnum.
587 sub cust_bill_pay_pkgnum {
588 my( $self, $pkgnum ) = @_;
589 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
590 sort { $a->_date <=> $b->_date }
591 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
597 =item cust_credited_pkgnum PKGNUM
599 =item cust_credit_bill_pkgnum PKGNUM
601 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
602 with matching pkgnum.
606 sub cust_credited_pkgnum {
607 my( $self, $pkgnum ) = @_;
608 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
609 sort { $a->_date <=> $b->_date }
610 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
616 sub cust_credit_bill_pkgnum {
617 shift->cust_credited_pkgnum(@_);
622 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
629 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
631 foreach (@taxlines) { $total += $_->setup; }
637 Returns the amount owed (still outstanding) on this invoice, which is charged
638 minus all payment applications (see L<FS::cust_bill_pay>) and credit
639 applications (see L<FS::cust_credit_bill>).
645 my $balance = $self->charged;
646 $balance -= $_->amount foreach ( $self->cust_bill_pay );
647 $balance -= $_->amount foreach ( $self->cust_credited );
648 $balance = sprintf( "%.2f", $balance);
649 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
654 my( $self, $pkgnum ) = @_;
656 #my $balance = $self->charged;
658 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
660 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
661 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
663 $balance = sprintf( "%.2f", $balance);
664 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
668 =item apply_payments_and_credits [ OPTION => VALUE ... ]
670 Applies unapplied payments and credits to this invoice.
672 A hash of optional arguments may be passed. Currently "manual" is supported.
673 If true, a payment receipt is sent instead of a statement when
674 'payment_receipt_email' configuration option is set.
676 If there is an error, returns the error, otherwise returns false.
680 sub apply_payments_and_credits {
681 my( $self, %options ) = @_;
683 local $SIG{HUP} = 'IGNORE';
684 local $SIG{INT} = 'IGNORE';
685 local $SIG{QUIT} = 'IGNORE';
686 local $SIG{TERM} = 'IGNORE';
687 local $SIG{TSTP} = 'IGNORE';
688 local $SIG{PIPE} = 'IGNORE';
690 my $oldAutoCommit = $FS::UID::AutoCommit;
691 local $FS::UID::AutoCommit = 0;
694 $self->select_for_update; #mutex
696 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
697 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
699 if ( $conf->exists('pkg-balances') ) {
700 # limit @payments & @credits to those w/ a pkgnum grepped from $self
701 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
702 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
703 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
706 while ( $self->owed > 0 and ( @payments || @credits ) ) {
709 if ( @payments && @credits ) {
711 #decide which goes first by weight of top (unapplied) line item
713 my @open_lineitems = $self->open_cust_bill_pkg;
716 max( map { $_->part_pkg->pay_weight || 0 }
721 my $max_credit_weight =
722 max( map { $_->part_pkg->credit_weight || 0 }
728 #if both are the same... payments first? it has to be something
729 if ( $max_pay_weight >= $max_credit_weight ) {
735 } elsif ( @payments ) {
737 } elsif ( @credits ) {
740 die "guru meditation #12 and 35";
744 if ( $app eq 'pay' ) {
746 my $payment = shift @payments;
747 $unapp_amount = $payment->unapplied;
748 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
749 $app->pkgnum( $payment->pkgnum )
750 if $conf->exists('pkg-balances') && $payment->pkgnum;
752 } elsif ( $app eq 'credit' ) {
754 my $credit = shift @credits;
755 $unapp_amount = $credit->credited;
756 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
757 $app->pkgnum( $credit->pkgnum )
758 if $conf->exists('pkg-balances') && $credit->pkgnum;
761 die "guru meditation #12 and 35";
765 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
766 warn "owed_pkgnum ". $app->pkgnum;
767 $owed = $self->owed_pkgnum($app->pkgnum);
771 next unless $owed > 0;
773 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
774 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
776 $app->invnum( $self->invnum );
778 my $error = $app->insert(%options);
780 $dbh->rollback if $oldAutoCommit;
781 return "Error inserting ". $app->table. " record: $error";
783 die $error if $error;
787 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
792 =item generate_email OPTION => VALUE ...
800 sender address, required
804 alternate template name, optional
808 text attachment arrayref, optional
812 email subject, optional
816 notice name instead of "Invoice", optional
820 Returns an argument list to be passed to L<FS::Misc::send_email>.
831 my $me = '[FS::cust_bill::generate_email]';
834 'from' => $args{'from'},
835 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
839 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
840 'template' => $args{'template'},
841 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
844 my $cust_main = $self->cust_main;
846 if (ref($args{'to'}) eq 'ARRAY') {
847 $return{'to'} = $args{'to'};
849 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
850 $cust_main->invoicing_list
854 if ( $conf->exists('invoice_html') ) {
856 warn "$me creating HTML/text multipart message"
859 $return{'nobody'} = 1;
861 my $alternative = build MIME::Entity
862 'Type' => 'multipart/alternative',
863 'Encoding' => '7bit',
864 'Disposition' => 'inline'
868 if ( $conf->exists('invoice_email_pdf')
869 and scalar($conf->config('invoice_email_pdf_note')) ) {
871 warn "$me using 'invoice_email_pdf_note' in multipart message"
873 $data = [ map { $_ . "\n" }
874 $conf->config('invoice_email_pdf_note')
879 warn "$me not using 'invoice_email_pdf_note' in multipart message"
881 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
882 $data = $args{'print_text'};
884 $data = [ $self->print_text(\%opt) ];
889 $alternative->attach(
890 'Type' => 'text/plain',
891 #'Encoding' => 'quoted-printable',
892 'Encoding' => '7bit',
894 'Disposition' => 'inline',
897 $args{'from'} =~ /\@([\w\.\-]+)/;
898 my $from = $1 || 'example.com';
899 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
902 my $agentnum = $cust_main->agentnum;
903 if ( defined($args{'template'}) && length($args{'template'})
904 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
907 $logo = 'logo_'. $args{'template'}. '.png';
911 my $image_data = $conf->config_binary( $logo, $agentnum);
913 my $image = build MIME::Entity
914 'Type' => 'image/png',
915 'Encoding' => 'base64',
916 'Data' => $image_data,
917 'Filename' => 'logo.png',
918 'Content-ID' => "<$content_id>",
921 $alternative->attach(
922 'Type' => 'text/html',
923 'Encoding' => 'quoted-printable',
924 'Data' => [ '<html>',
927 ' '. encode_entities($return{'subject'}),
930 ' <body bgcolor="#e8e8e8">',
931 $self->print_html({ 'cid'=>$content_id, %opt }),
935 'Disposition' => 'inline',
936 #'Filename' => 'invoice.pdf',
940 if ( $cust_main->email_csv_cdr ) {
942 push @otherparts, build MIME::Entity
943 'Type' => 'text/csv',
944 'Encoding' => '7bit',
945 'Data' => [ map { "$_\n" }
946 $self->call_details('prepend_billed_number' => 1)
948 'Disposition' => 'attachment',
949 'Filename' => 'usage-'. $self->invnum. '.csv',
954 if ( $conf->exists('invoice_email_pdf') ) {
959 # multipart/alternative
965 my $related = build MIME::Entity 'Type' => 'multipart/related',
966 'Encoding' => '7bit';
968 #false laziness w/Misc::send_email
969 $related->head->replace('Content-type',
971 '; boundary="'. $related->head->multipart_boundary. '"'.
972 '; type=multipart/alternative'
975 $related->add_part($alternative);
977 $related->add_part($image);
979 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
981 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
985 #no other attachment:
987 # multipart/alternative
992 $return{'content-type'} = 'multipart/related';
993 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
994 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
995 #$return{'disposition'} = 'inline';
1001 if ( $conf->exists('invoice_email_pdf') ) {
1002 warn "$me creating PDF attachment"
1005 #mime parts arguments a la MIME::Entity->build().
1006 $return{'mimeparts'} = [
1007 { $self->mimebuild_pdf(\%opt) }
1011 if ( $conf->exists('invoice_email_pdf')
1012 and scalar($conf->config('invoice_email_pdf_note')) ) {
1014 warn "$me using 'invoice_email_pdf_note'"
1016 $return{'body'} = [ map { $_ . "\n" }
1017 $conf->config('invoice_email_pdf_note')
1022 warn "$me not using 'invoice_email_pdf_note'"
1024 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1025 $return{'body'} = $args{'print_text'};
1027 $return{'body'} = [ $self->print_text(\%opt) ];
1040 Returns a list suitable for passing to MIME::Entity->build(), representing
1041 this invoice as PDF attachment.
1048 'Type' => 'application/pdf',
1049 'Encoding' => 'base64',
1050 'Data' => [ $self->print_pdf(@_) ],
1051 'Disposition' => 'attachment',
1052 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1056 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1058 Sends this invoice to the destinations configured for this customer: sends
1059 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1061 Options can be passed as a hashref (recommended) or as a list of up to
1062 four values for templatename, agentnum, invoice_from and amount.
1064 I<template>, if specified, is the name of a suffix for alternate invoices.
1066 I<agentnum>, if specified, means that this invoice will only be sent for customers
1067 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1068 single agent) or an arrayref of agentnums.
1070 I<invoice_from>, if specified, overrides the default email invoice From: address.
1072 I<amount>, if specified, only sends the invoice if the total amount owed on this
1073 invoice and all older invoices is greater than the specified amount.
1075 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1079 sub queueable_send {
1082 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1083 or die "invalid invoice number: " . $opt{invnum};
1085 my @args = ( $opt{template}, $opt{agentnum} );
1086 push @args, $opt{invoice_from}
1087 if exists($opt{invoice_from}) && $opt{invoice_from};
1089 my $error = $self->send( @args );
1090 die $error if $error;
1097 my( $template, $invoice_from, $notice_name );
1099 my $balance_over = 0;
1103 $template = $opt->{'template'} || '';
1104 if ( $agentnums = $opt->{'agentnum'} ) {
1105 $agentnums = [ $agentnums ] unless ref($agentnums);
1107 $invoice_from = $opt->{'invoice_from'};
1108 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1109 $notice_name = $opt->{'notice_name'};
1111 $template = scalar(@_) ? shift : '';
1112 if ( scalar(@_) && $_[0] ) {
1113 $agentnums = ref($_[0]) ? shift : [ shift ];
1115 $invoice_from = shift if scalar(@_);
1116 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1119 return 'N/A' unless ! $agentnums
1120 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1123 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1125 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1126 $conf->config('invoice_from', $self->cust_main->agentnum );
1129 'template' => $template,
1130 'invoice_from' => $invoice_from,
1131 'notice_name' => ( $notice_name || 'Invoice' ),
1134 my @invoicing_list = $self->cust_main->invoicing_list;
1136 #$self->email_invoice(\%opt)
1138 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1140 #$self->print_invoice(\%opt)
1142 if grep { $_ eq 'POST' } @invoicing_list; #postal
1144 $self->fax_invoice(\%opt)
1145 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1151 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1153 Emails this invoice.
1155 Options can be passed as a hashref (recommended) or as a list of up to
1156 two values for templatename and invoice_from.
1158 I<template>, if specified, is the name of a suffix for alternate invoices.
1160 I<invoice_from>, if specified, overrides the default email invoice From: address.
1162 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1166 sub queueable_email {
1169 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1170 or die "invalid invoice number: " . $opt{invnum};
1172 my @args = ( $opt{template} );
1173 push @args, $opt{invoice_from}
1174 if exists($opt{invoice_from}) && $opt{invoice_from};
1176 my $error = $self->email( @args );
1177 die $error if $error;
1181 #sub email_invoice {
1185 my( $template, $invoice_from, $notice_name );
1188 $template = $opt->{'template'} || '';
1189 $invoice_from = $opt->{'invoice_from'};
1190 $notice_name = $opt->{'notice_name'} || 'Invoice';
1192 $template = scalar(@_) ? shift : '';
1193 $invoice_from = shift if scalar(@_);
1194 $notice_name = 'Invoice';
1197 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1198 $conf->config('invoice_from', $self->cust_main->agentnum );
1200 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1201 $self->cust_main->invoicing_list;
1203 #better to notify this person than silence
1204 @invoicing_list = ($invoice_from) unless @invoicing_list;
1206 my $subject = $self->email_subject($template);
1208 my $error = send_email(
1209 $self->generate_email(
1210 'from' => $invoice_from,
1211 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1212 'subject' => $subject,
1213 'template' => $template,
1214 'notice_name' => $notice_name,
1217 die "can't email invoice: $error\n" if $error;
1218 #die "$error\n" if $error;
1225 #my $template = scalar(@_) ? shift : '';
1228 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1231 my $cust_main = $self->cust_main;
1232 my $name = $cust_main->name;
1233 my $name_short = $cust_main->name_short;
1234 my $invoice_number = $self->invnum;
1235 my $invoice_date = $self->_date_pretty;
1237 eval qq("$subject");
1240 =item lpr_data HASHREF | [ TEMPLATE ]
1242 Returns the postscript or plaintext for this invoice as an arrayref.
1244 Options can be passed as a hashref (recommended) or as a single optional value
1247 I<template>, if specified, is the name of a suffix for alternate invoices.
1249 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1255 my( $template, $notice_name );
1258 $template = $opt->{'template'} || '';
1259 $notice_name = $opt->{'notice_name'} || 'Invoice';
1261 $template = scalar(@_) ? shift : '';
1262 $notice_name = 'Invoice';
1266 'template' => $template,
1267 'notice_name' => $notice_name,
1270 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1271 [ $self->$method( \%opt ) ];
1274 =item print HASHREF | [ TEMPLATE ]
1276 Prints this invoice.
1278 Options can be passed as a hashref (recommended) or as a single optional
1281 I<template>, if specified, is the name of a suffix for alternate invoices.
1283 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1287 #sub print_invoice {
1290 my( $template, $notice_name );
1293 $template = $opt->{'template'} || '';
1294 $notice_name = $opt->{'notice_name'} || 'Invoice';
1296 $template = scalar(@_) ? shift : '';
1297 $notice_name = 'Invoice';
1301 'template' => $template,
1302 'notice_name' => $notice_name,
1305 if($conf->exists('invoice_print_pdf')) {
1306 # Add the invoice to the current batch.
1307 $self->batch_invoice(\%opt);
1310 do_print $self->lpr_data(\%opt);
1314 =item fax_invoice HASHREF | [ TEMPLATE ]
1318 Options can be passed as a hashref (recommended) or as a single optional
1321 I<template>, if specified, is the name of a suffix for alternate invoices.
1323 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1329 my( $template, $notice_name );
1332 $template = $opt->{'template'} || '';
1333 $notice_name = $opt->{'notice_name'} || 'Invoice';
1335 $template = scalar(@_) ? shift : '';
1336 $notice_name = 'Invoice';
1339 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1340 unless $conf->exists('invoice_latex');
1342 my $dialstring = $self->cust_main->getfield('fax');
1346 'template' => $template,
1347 'notice_name' => $notice_name,
1350 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1351 'dialstring' => $dialstring,
1353 die $error if $error;
1357 =item batch_invoice [ HASHREF ]
1359 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1360 isn't an open batch, one will be created.
1365 my ($self, $opt) = @_;
1366 my $batch = FS::bill_batch->get_open_batch;
1367 my $cust_bill_batch = FS::cust_bill_batch->new({
1368 batchnum => $batch->batchnum,
1369 invnum => $self->invnum,
1371 return $cust_bill_batch->insert($opt);
1374 =item ftp_invoice [ TEMPLATENAME ]
1376 Sends this invoice data via FTP.
1378 TEMPLATENAME is unused?
1384 my $template = scalar(@_) ? shift : '';
1387 'protocol' => 'ftp',
1388 'server' => $conf->config('cust_bill-ftpserver'),
1389 'username' => $conf->config('cust_bill-ftpusername'),
1390 'password' => $conf->config('cust_bill-ftppassword'),
1391 'dir' => $conf->config('cust_bill-ftpdir'),
1392 'format' => $conf->config('cust_bill-ftpformat'),
1396 =item spool_invoice [ TEMPLATENAME ]
1398 Spools this invoice data (see L<FS::spool_csv>)
1400 TEMPLATENAME is unused?
1406 my $template = scalar(@_) ? shift : '';
1409 'format' => $conf->config('cust_bill-spoolformat'),
1410 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1414 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1416 Like B<send>, but only sends the invoice if it is the newest open invoice for
1421 sub send_if_newest {
1426 grep { $_->owed > 0 }
1427 qsearch('cust_bill', {
1428 'custnum' => $self->custnum,
1429 #'_date' => { op=>'>', value=>$self->_date },
1430 'invnum' => { op=>'>', value=>$self->invnum },
1437 =item send_csv OPTION => VALUE, ...
1439 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1443 protocol - currently only "ftp"
1449 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1450 and YYMMDDHHMMSS is a timestamp.
1452 See L</print_csv> for a description of the output format.
1457 my($self, %opt) = @_;
1461 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1462 mkdir $spooldir, 0700 unless -d $spooldir;
1464 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1465 my $file = "$spooldir/$tracctnum.csv";
1467 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1469 open(CSV, ">$file") or die "can't open $file: $!";
1477 if ( $opt{protocol} eq 'ftp' ) {
1478 eval "use Net::FTP;";
1480 $net = Net::FTP->new($opt{server}) or die @$;
1482 die "unknown protocol: $opt{protocol}";
1485 $net->login( $opt{username}, $opt{password} )
1486 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1488 $net->binary or die "can't set binary mode";
1490 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1492 $net->put($file) or die "can't put $file: $!";
1502 Spools CSV invoice data.
1508 =item format - 'default' or 'billco'
1510 =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>).
1512 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1514 =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.
1521 my($self, %opt) = @_;
1523 my $cust_main = $self->cust_main;
1525 if ( $opt{'dest'} ) {
1526 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1527 $cust_main->invoicing_list;
1528 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1529 || ! keys %invoicing_list;
1532 if ( $opt{'balanceover'} ) {
1534 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1537 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1538 mkdir $spooldir, 0700 unless -d $spooldir;
1540 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1544 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1545 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1548 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1550 open(CSV, ">>$file") or die "can't open $file: $!";
1551 flock(CSV, LOCK_EX);
1556 if ( lc($opt{'format'}) eq 'billco' ) {
1558 flock(CSV, LOCK_UN);
1563 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1566 open(CSV,">>$file") or die "can't open $file: $!";
1567 flock(CSV, LOCK_EX);
1573 flock(CSV, LOCK_UN);
1580 =item print_csv OPTION => VALUE, ...
1582 Returns CSV data for this invoice.
1586 format - 'default' or 'billco'
1588 Returns a list consisting of two scalars. The first is a single line of CSV
1589 header information for this invoice. The second is one or more lines of CSV
1590 detail information for this invoice.
1592 If I<format> is not specified or "default", the fields of the CSV file are as
1595 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1599 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1601 B<record_type> is C<cust_bill> for the initial header line only. The
1602 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1603 fields are filled in.
1605 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1606 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1609 =item invnum - invoice number
1611 =item custnum - customer number
1613 =item _date - invoice date
1615 =item charged - total invoice amount
1617 =item first - customer first name
1619 =item last - customer first name
1621 =item company - company name
1623 =item address1 - address line 1
1625 =item address2 - address line 1
1635 =item pkg - line item description
1637 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1639 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1641 =item sdate - start date for recurring fee
1643 =item edate - end date for recurring fee
1647 If I<format> is "billco", the fields of the header CSV file are as follows:
1649 +-------------------------------------------------------------------+
1650 | FORMAT HEADER FILE |
1651 |-------------------------------------------------------------------|
1652 | Field | Description | Name | Type | Width |
1653 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1654 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1655 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1656 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1657 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1658 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1659 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1660 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1661 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1662 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1663 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1664 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1665 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1666 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1667 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1668 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1669 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1670 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1671 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1672 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1673 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1674 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1675 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1676 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1677 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1678 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1679 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1680 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1681 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1682 +-------+-------------------------------+------------+------+-------+
1684 If I<format> is "billco", the fields of the detail CSV file are as follows:
1686 FORMAT FOR DETAIL FILE
1688 Field | Description | Name | Type | Width
1689 1 | N/A-Leave Empty | RC | CHAR | 2
1690 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1691 3 | Account Number | TRACCTNUM | CHAR | 15
1692 4 | Invoice Number | TRINVOICE | CHAR | 15
1693 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1694 6 | Transaction Detail | DETAILS | CHAR | 100
1695 7 | Amount | AMT | NUM* | 9
1696 8 | Line Format Control** | LNCTRL | CHAR | 2
1697 9 | Grouping Code | GROUP | CHAR | 2
1698 10 | User Defined | ACCT CODE | CHAR | 15
1703 my($self, %opt) = @_;
1705 eval "use Text::CSV_XS";
1708 my $cust_main = $self->cust_main;
1710 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1712 if ( lc($opt{'format'}) eq 'billco' ) {
1715 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1717 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1719 my( $previous_balance, @unused ) = $self->previous; #previous balance
1721 my $pmt_cr_applied = 0;
1722 $pmt_cr_applied += $_->{'amount'}
1723 foreach ( $self->_items_payments, $self->_items_credits ) ;
1725 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1728 '', # 1 | N/A-Leave Empty CHAR 2
1729 '', # 2 | N/A-Leave Empty CHAR 15
1730 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1731 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1732 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1733 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1734 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1735 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1736 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1737 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1738 '', # 10 | Ancillary Billing Information CHAR 30
1739 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1740 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1743 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1746 $duedate, # 14 | Bill Due Date CHAR 10
1748 $previous_balance, # 15 | Previous Balance NUM* 9
1749 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1750 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1751 $totaldue, # 18 | Total Amt Due NUM* 9
1752 $totaldue, # 19 | Total Amt Due NUM* 9
1753 '', # 20 | 30 Day Aging NUM* 9
1754 '', # 21 | 60 Day Aging NUM* 9
1755 '', # 22 | 90 Day Aging NUM* 9
1756 'N', # 23 | Y/N CHAR 1
1757 '', # 24 | Remittance automation CHAR 100
1758 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1759 $self->custnum, # 26 | Customer Reference Number CHAR 15
1760 '0', # 27 | Federal Tax*** NUM* 9
1761 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1762 '0', # 29 | Other Taxes & Fees*** NUM* 9
1771 time2str("%x", $self->_date),
1772 sprintf("%.2f", $self->charged),
1773 ( map { $cust_main->getfield($_) }
1774 qw( first last company address1 address2 city state zip country ) ),
1776 ) or die "can't create csv";
1779 my $header = $csv->string. "\n";
1782 if ( lc($opt{'format'}) eq 'billco' ) {
1785 foreach my $item ( $self->_items_pkg ) {
1788 '', # 1 | N/A-Leave Empty CHAR 2
1789 '', # 2 | N/A-Leave Empty CHAR 15
1790 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1791 $self->invnum, # 4 | Invoice Number CHAR 15
1792 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1793 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1794 $item->{'amount'}, # 7 | Amount NUM* 9
1795 '', # 8 | Line Format Control** CHAR 2
1796 '', # 9 | Grouping Code CHAR 2
1797 '', # 10 | User Defined CHAR 15
1800 $detail .= $csv->string. "\n";
1806 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1808 my($pkg, $setup, $recur, $sdate, $edate);
1809 if ( $cust_bill_pkg->pkgnum ) {
1811 ($pkg, $setup, $recur, $sdate, $edate) = (
1812 $cust_bill_pkg->part_pkg->pkg,
1813 ( $cust_bill_pkg->setup != 0
1814 ? sprintf("%.2f", $cust_bill_pkg->setup )
1816 ( $cust_bill_pkg->recur != 0
1817 ? sprintf("%.2f", $cust_bill_pkg->recur )
1819 ( $cust_bill_pkg->sdate
1820 ? time2str("%x", $cust_bill_pkg->sdate)
1822 ($cust_bill_pkg->edate
1823 ?time2str("%x", $cust_bill_pkg->edate)
1827 } else { #pkgnum tax
1828 next unless $cust_bill_pkg->setup != 0;
1829 $pkg = $cust_bill_pkg->desc;
1830 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1831 ( $sdate, $edate ) = ( '', '' );
1837 ( map { '' } (1..11) ),
1838 ($pkg, $setup, $recur, $sdate, $edate)
1839 ) or die "can't create csv";
1841 $detail .= $csv->string. "\n";
1847 ( $header, $detail );
1853 Pays this invoice with a compliemntary payment. If there is an error,
1854 returns the error, otherwise returns false.
1860 my $cust_pay = new FS::cust_pay ( {
1861 'invnum' => $self->invnum,
1862 'paid' => $self->owed,
1865 'payinfo' => $self->cust_main->payinfo,
1873 Attempts to pay this invoice with a credit card payment via a
1874 Business::OnlinePayment realtime gateway. See
1875 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1876 for supported processors.
1882 $self->realtime_bop( 'CC', @_ );
1887 Attempts to pay this invoice with an electronic check (ACH) payment via a
1888 Business::OnlinePayment realtime gateway. See
1889 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1890 for supported processors.
1896 $self->realtime_bop( 'ECHECK', @_ );
1901 Attempts to pay this invoice with phone bill (LEC) payment via a
1902 Business::OnlinePayment realtime gateway. See
1903 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1904 for supported processors.
1910 $self->realtime_bop( 'LEC', @_ );
1914 my( $self, $method ) = @_;
1916 my $cust_main = $self->cust_main;
1917 my $balance = $cust_main->balance;
1918 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1919 $amount = sprintf("%.2f", $amount);
1920 return "not run (balance $balance)" unless $amount > 0;
1922 my $description = 'Internet Services';
1923 if ( $conf->exists('business-onlinepayment-description') ) {
1924 my $dtempl = $conf->config('business-onlinepayment-description');
1926 my $agent_obj = $cust_main->agent
1927 or die "can't retreive agent for $cust_main (agentnum ".
1928 $cust_main->agentnum. ")";
1929 my $agent = $agent_obj->agent;
1930 my $pkgs = join(', ',
1931 map { $_->part_pkg->pkg }
1932 grep { $_->pkgnum } $self->cust_bill_pkg
1934 $description = eval qq("$dtempl");
1937 $cust_main->realtime_bop($method, $amount,
1938 'description' => $description,
1939 'invnum' => $self->invnum,
1940 #this didn't do what we want, it just calls apply_payments_and_credits
1942 'apply_to_invoice' => 1,
1944 #this changes application behavior: auto payments
1945 #triggered against a specific invoice are now applied
1946 #to that invoice instead of oldest open.
1952 =item batch_card OPTION => VALUE...
1954 Adds a payment for this invoice to the pending credit card batch (see
1955 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1956 runs the payment using a realtime gateway.
1961 my ($self, %options) = @_;
1962 my $cust_main = $self->cust_main;
1964 $options{invnum} = $self->invnum;
1966 $cust_main->batch_card(%options);
1969 sub _agent_template {
1971 $self->cust_main->agent_template;
1974 sub _agent_invoice_from {
1976 $self->cust_main->agent_invoice_from;
1979 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1981 Returns an text invoice, as a list of lines.
1983 Options can be passed as a hashref (recommended) or as a list of time, template
1984 and then any key/value pairs for any other options.
1986 I<time>, if specified, is used to control the printing of overdue messages. The
1987 default is now. It isn't the date of the invoice; that's the `_date' field.
1988 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1989 L<Time::Local> and L<Date::Parse> for conversion functions.
1991 I<template>, if specified, is the name of a suffix for alternate invoices.
1993 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1999 my( $today, $template, %opt );
2001 %opt = %{ shift() };
2002 $today = delete($opt{'time'}) || '';
2003 $template = delete($opt{template}) || '';
2005 ( $today, $template, %opt ) = @_;
2008 my %params = ( 'format' => 'template' );
2009 $params{'time'} = $today if $today;
2010 $params{'template'} = $template if $template;
2011 $params{$_} = $opt{$_}
2012 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2014 $self->print_generic( %params );
2017 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2019 Internal method - returns a filename of a filled-in LaTeX template for this
2020 invoice (Note: add ".tex" to get the actual filename), and a filename of
2021 an associated logo (with the .eps extension included).
2023 See print_ps and print_pdf for methods that return PostScript and PDF output.
2025 Options can be passed as a hashref (recommended) or as a list of time, template
2026 and then any key/value pairs for any other options.
2028 I<time>, if specified, is used to control the printing of overdue messages. The
2029 default is now. It isn't the date of the invoice; that's the `_date' field.
2030 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2031 L<Time::Local> and L<Date::Parse> for conversion functions.
2033 I<template>, if specified, is the name of a suffix for alternate invoices.
2035 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2041 my( $today, $template, %opt );
2043 %opt = %{ shift() };
2044 $today = delete($opt{'time'}) || '';
2045 $template = delete($opt{template}) || '';
2047 ( $today, $template, %opt ) = @_;
2050 my %params = ( 'format' => 'latex' );
2051 $params{'time'} = $today if $today;
2052 $params{'template'} = $template if $template;
2053 $params{$_} = $opt{$_}
2054 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2056 $template ||= $self->_agent_template;
2058 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2059 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2063 ) or die "can't open temp file: $!\n";
2065 my $agentnum = $self->cust_main->agentnum;
2067 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2068 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2069 or die "can't write temp file: $!\n";
2071 print $lh $conf->config_binary('logo.eps', $agentnum)
2072 or die "can't write temp file: $!\n";
2075 $params{'logo_file'} = $lh->filename;
2077 my @filled_in = $self->print_generic( %params );
2079 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2083 ) or die "can't open temp file: $!\n";
2084 print $fh join('', @filled_in );
2087 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2088 return ($1, $params{'logo_file'});
2092 =item print_generic OPTION => VALUE ...
2094 Internal method - returns a filled-in template for this invoice as a scalar.
2096 See print_ps and print_pdf for methods that return PostScript and PDF output.
2098 Non optional options include
2099 format - latex, html, template
2101 Optional options include
2103 template - a value used as a suffix for a configuration template
2105 time - a value used to control the printing of overdue messages. The
2106 default is now. It isn't the date of the invoice; that's the `_date' field.
2107 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2108 L<Time::Local> and L<Date::Parse> for conversion functions.
2112 unsquelch_cdr - overrides any per customer cdr squelching when true
2114 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2118 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2119 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2120 # yes: fixed width (dot matrix) text printing will be borked
2123 my( $self, %params ) = @_;
2124 my $today = $params{today} ? $params{today} : time;
2125 warn "$me print_generic called on $self with suffix $params{template}\n"
2128 my $format = $params{format};
2129 die "Unknown format: $format"
2130 unless $format =~ /^(latex|html|template)$/;
2132 my $cust_main = $self->cust_main;
2133 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2134 unless $cust_main->payname
2135 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2137 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2138 'html' => [ '<%=', '%>' ],
2139 'template' => [ '{', '}' ],
2142 #create the template
2143 my $template = $params{template} ? $params{template} : $self->_agent_template;
2144 my $templatefile = "invoice_$format";
2145 $templatefile .= "_$template"
2146 if length($template);
2147 my @invoice_template = map "$_\n", $conf->config($templatefile)
2148 or die "cannot load config data $templatefile";
2151 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2152 #change this to a die when the old code is removed
2153 warn "old-style invoice template $templatefile; ".
2154 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2155 $old_latex = 'true';
2156 @invoice_template = _translate_old_latex_format(@invoice_template);
2159 my $text_template = new Text::Template(
2161 SOURCE => \@invoice_template,
2162 DELIMITERS => $delimiters{$format},
2165 $text_template->compile()
2166 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2169 # additional substitution could possibly cause breakage in existing templates
2170 my %convert_maps = (
2172 'notes' => sub { map "$_", @_ },
2173 'footer' => sub { map "$_", @_ },
2174 'smallfooter' => sub { map "$_", @_ },
2175 'returnaddress' => sub { map "$_", @_ },
2176 'coupon' => sub { map "$_", @_ },
2177 'summary' => sub { map "$_", @_ },
2183 s/%%(.*)$/<!-- $1 -->/g;
2184 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2185 s/\\begin\{enumerate\}/<ol>/g;
2187 s/\\end\{enumerate\}/<\/ol>/g;
2188 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2197 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2199 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2204 s/\\\\\*?\s*$/<BR>/;
2205 s/\\hyphenation\{[\w\s\-]+}//;
2210 'coupon' => sub { "" },
2211 'summary' => sub { "" },
2218 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2219 s/\\begin\{enumerate\}//g;
2221 s/\\end\{enumerate\}//g;
2222 s/\\textbf\{(.*)\}/$1/g;
2229 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2231 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2236 s/\\\\\*?\s*$/\n/; # dubious
2237 s/\\hyphenation\{[\w\s\-]+}//;
2241 'coupon' => sub { "" },
2242 'summary' => sub { "" },
2247 # hashes for differing output formats
2248 my %nbsps = ( 'latex' => '~',
2249 'html' => '', # '&nbps;' would be nice
2250 'template' => '', # not used
2252 my $nbsp = $nbsps{$format};
2254 my %escape_functions = ( 'latex' => \&_latex_escape,
2255 'html' => \&encode_entities,
2256 'template' => sub { shift },
2258 my $escape_function = $escape_functions{$format};
2260 my %date_formats = ( 'latex' => '%b %o, %Y',
2261 'html' => '%b %o, %Y',
2264 my $date_format = $date_formats{$format};
2266 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2268 'html' => sub { return '<b>'. shift(). '</b>'
2270 'template' => sub { shift },
2272 my $embolden_function = $embolden_functions{$format};
2275 # generate template variables
2278 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2282 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2288 $returnaddress = join("\n",
2289 $conf->config_orbase("invoice_${format}returnaddress", $template)
2292 } elsif ( grep /\S/,
2293 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2295 my $convert_map = $convert_maps{$format}{'returnaddress'};
2298 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2303 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2305 my $convert_map = $convert_maps{$format}{'returnaddress'};
2306 $returnaddress = join( "\n", &$convert_map(
2307 map { s/( {2,})/'~' x length($1)/eg;
2311 ( $conf->config('company_name', $self->cust_main->agentnum),
2312 $conf->config('company_address', $self->cust_main->agentnum),
2319 my $warning = "Couldn't find a return address; ".
2320 "do you need to set the company_address configuration value?";
2322 $returnaddress = $nbsp;
2323 #$returnaddress = $warning;
2327 my $agentnum = $self->cust_main->agentnum;
2329 my %invoice_data = (
2332 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2333 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2334 'returnaddress' => $returnaddress,
2335 'agent' => &$escape_function($cust_main->agent->agent),
2338 'invnum' => $self->invnum,
2339 'date' => time2str($date_format, $self->_date),
2340 'today' => time2str('%b %o, %Y', $today),
2341 'terms' => $self->terms,
2342 'template' => $template, #params{'template'},
2343 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2344 'current_charges' => sprintf("%.2f", $self->charged),
2345 'duedate' => $self->due_date2str($rdate_format), #date_format?
2348 'custnum' => $cust_main->display_custnum,
2349 'agent_custid' => &$escape_function($cust_main->agent_custid),
2350 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2351 payname company address1 address2 city state zip fax
2355 'ship_enable' => $conf->exists('invoice-ship_address'),
2356 'unitprices' => $conf->exists('invoice-unitprice'),
2357 'smallernotes' => $conf->exists('invoice-smallernotes'),
2358 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2359 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2361 #layout info -- would be fancy to calc some of this and bury the template
2363 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2364 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2365 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2366 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2367 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2368 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2369 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2370 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2371 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2372 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2374 # better hang on to conf_dir for a while (for old templates)
2375 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2377 #these are only used when doing paged plaintext
2383 $invoice_data{finance_section} = '';
2384 if ( $conf->config('finance_pkgclass') ) {
2386 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2387 $invoice_data{finance_section} = $pkg_class->categoryname;
2389 $invoice_data{finance_amount} = '0.00';
2390 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2392 my $countrydefault = $conf->config('countrydefault') || 'US';
2393 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2394 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2395 my $method = $prefix.$_;
2396 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2398 $invoice_data{'ship_country'} = ''
2399 if ( $invoice_data{'ship_country'} eq $countrydefault );
2401 $invoice_data{'cid'} = $params{'cid'}
2404 if ( $cust_main->country eq $countrydefault ) {
2405 $invoice_data{'country'} = '';
2407 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2411 $invoice_data{'address'} = \@address;
2413 $cust_main->payname.
2414 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2415 ? " (P.O. #". $cust_main->payinfo. ")"
2419 push @address, $cust_main->company
2420 if $cust_main->company;
2421 push @address, $cust_main->address1;
2422 push @address, $cust_main->address2
2423 if $cust_main->address2;
2425 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2426 push @address, $invoice_data{'country'}
2427 if $invoice_data{'country'};
2429 while (scalar(@address) < 5);
2431 $invoice_data{'logo_file'} = $params{'logo_file'}
2432 if $params{'logo_file'};
2434 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2435 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2436 #my $balance_due = $self->owed + $pr_total - $cr_total;
2437 my $balance_due = $self->owed + $pr_total;
2438 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2439 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2440 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2441 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2443 my $summarypage = '';
2444 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2447 $invoice_data{'summarypage'} = $summarypage;
2449 #do variable substitution in notes, footer, smallfooter
2450 foreach my $include (qw( notes footer smallfooter coupon )) {
2452 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2455 if ( $conf->exists($inc_file, $agentnum)
2456 && length( $conf->config($inc_file, $agentnum) ) ) {
2458 @inc_src = $conf->config($inc_file, $agentnum);
2462 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2464 my $convert_map = $convert_maps{$format}{$include};
2466 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2467 s/--\@\]/$delimiters{$format}[1]/g;
2470 &$convert_map( $conf->config($inc_file, $agentnum) );
2474 my $inc_tt = new Text::Template (
2476 SOURCE => [ map "$_\n", @inc_src ],
2477 DELIMITERS => $delimiters{$format},
2478 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2480 unless ( $inc_tt->compile() ) {
2481 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2482 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2486 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2488 $invoice_data{$include} =~ s/\n+$//
2489 if ($format eq 'latex');
2492 $invoice_data{'po_line'} =
2493 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2494 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2497 my %money_chars = ( 'latex' => '',
2498 'html' => $conf->config('money_char') || '$',
2501 my $money_char = $money_chars{$format};
2503 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2504 'html' => $conf->config('money_char') || '$',
2507 my $other_money_char = $other_money_chars{$format};
2508 $invoice_data{'dollar'} = $other_money_char;
2510 my @detail_items = ();
2511 my @total_items = ();
2515 $invoice_data{'detail_items'} = \@detail_items;
2516 $invoice_data{'total_items'} = \@total_items;
2517 $invoice_data{'buf'} = \@buf;
2518 $invoice_data{'sections'} = \@sections;
2520 my $previous_section = { 'description' => 'Previous Charges',
2521 'subtotal' => $other_money_char.
2522 sprintf('%.2f', $pr_total),
2523 'summarized' => $summarypage ? 'Y' : '',
2525 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2526 join(' / ', map { $cust_main->balance_date_range(@$_) }
2527 $self->_prior_month30s
2529 if $conf->exists('invoice_include_aging');
2532 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2533 'subtotal' => $taxtotal, # adjusted below
2534 'summarized' => $summarypage ? 'Y' : '',
2536 my $tax_weight = _pkg_category($tax_section->{description})
2537 ? _pkg_category($tax_section->{description})->weight
2539 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2540 $tax_section->{'sort_weight'} = $tax_weight;
2543 my $adjusttotal = 0;
2544 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2545 'subtotal' => 0, # adjusted below
2546 'summarized' => $summarypage ? 'Y' : '',
2548 my $adjust_weight = _pkg_category($adjust_section->{description})
2549 ? _pkg_category($adjust_section->{description})->weight
2551 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2552 $adjust_section->{'sort_weight'} = $adjust_weight;
2554 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2555 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2556 $invoice_data{'multisection'} = $multisection;
2557 my $late_sections = [];
2558 my $extra_sections = [];
2559 my $extra_lines = ();
2560 if ( $multisection ) {
2561 ($extra_sections, $extra_lines) =
2562 $self->_items_extra_usage_sections($escape_function, $format)
2563 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2565 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2567 push @detail_items, @$extra_lines if $extra_lines;
2569 $self->_items_sections( $late_sections, # this could stand a refactor
2575 if ($conf->exists('svc_phone_sections')) {
2576 my ($phone_sections, $phone_lines) =
2577 $self->_items_svc_phone_sections($escape_function, $format);
2578 push @{$late_sections}, @$phone_sections;
2579 push @detail_items, @$phone_lines;
2582 push @sections, { 'description' => '', 'subtotal' => '' };
2585 unless ( $conf->exists('disable_previous_balance')
2586 || $conf->exists('previous_balance-summary_only')
2590 foreach my $line_item ( $self->_items_previous ) {
2593 ext_description => [],
2595 $detail->{'ref'} = $line_item->{'pkgnum'};
2596 $detail->{'quantity'} = 1;
2597 $detail->{'section'} = $previous_section;
2598 $detail->{'description'} = &$escape_function($line_item->{'description'});
2599 if ( exists $line_item->{'ext_description'} ) {
2600 @{$detail->{'ext_description'}} = map {
2601 &$escape_function($_);
2602 } @{$line_item->{'ext_description'}};
2604 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2605 $line_item->{'amount'};
2606 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2608 push @detail_items, $detail;
2609 push @buf, [ $detail->{'description'},
2610 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2616 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2617 push @buf, ['','-----------'];
2618 push @buf, [ 'Total Previous Balance',
2619 $money_char. sprintf("%10.2f", $pr_total) ];
2623 foreach my $section (@sections, @$late_sections) {
2625 # begin some normalization
2626 $section->{'subtotal'} = $section->{'amount'}
2628 && !exists($section->{subtotal})
2629 && exists($section->{amount});
2631 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2632 if ( $invoice_data{finance_section} &&
2633 $section->{'description'} eq $invoice_data{finance_section} );
2635 $section->{'subtotal'} = $other_money_char.
2636 sprintf('%.2f', $section->{'subtotal'})
2639 # continue some normalization
2640 $section->{'amount'} = $section->{'subtotal'}
2644 if ( $section->{'description'} ) {
2645 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2650 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2652 $options{'section'} = $section if $multisection;
2653 $options{'format'} = $format;
2654 $options{'escape_function'} = $escape_function;
2655 $options{'format_function'} = sub { () } unless $unsquelched;
2656 $options{'unsquelched'} = $unsquelched;
2657 $options{'summary_page'} = $summarypage;
2658 $options{'skip_usage'} =
2659 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2660 $options{'multilocation'} = $multilocation;
2661 $options{'multisection'} = $multisection;
2663 foreach my $line_item ( $self->_items_pkg(%options) ) {
2665 ext_description => [],
2667 $detail->{'ref'} = $line_item->{'pkgnum'};
2668 $detail->{'quantity'} = $line_item->{'quantity'};
2669 $detail->{'section'} = $section;
2670 $detail->{'description'} = &$escape_function($line_item->{'description'});
2671 if ( exists $line_item->{'ext_description'} ) {
2672 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2674 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2675 $line_item->{'amount'};
2676 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2677 $line_item->{'unit_amount'};
2678 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2680 push @detail_items, $detail;
2681 push @buf, ( [ $detail->{'description'},
2682 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2684 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2688 if ( $section->{'description'} ) {
2689 push @buf, ( ['','-----------'],
2690 [ $section->{'description'}. ' sub-total',
2691 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2700 $invoice_data{current_less_finance} =
2701 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2703 if ( $multisection && !$conf->exists('disable_previous_balance')
2704 || $conf->exists('previous_balance-summary_only') )
2706 unshift @sections, $previous_section if $pr_total;
2709 foreach my $tax ( $self->_items_tax ) {
2711 $taxtotal += $tax->{'amount'};
2713 my $description = &$escape_function( $tax->{'description'} );
2714 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2716 if ( $multisection ) {
2718 my $money = $old_latex ? '' : $money_char;
2719 push @detail_items, {
2720 ext_description => [],
2723 description => $description,
2724 amount => $money. $amount,
2726 section => $tax_section,
2731 push @total_items, {
2732 'total_item' => $description,
2733 'total_amount' => $other_money_char. $amount,
2738 push @buf,[ $description,
2739 $money_char. $amount,
2746 $total->{'total_item'} = 'Sub-total';
2747 $total->{'total_amount'} =
2748 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2750 if ( $multisection ) {
2751 $tax_section->{'subtotal'} = $other_money_char.
2752 sprintf('%.2f', $taxtotal);
2753 $tax_section->{'pretotal'} = 'New charges sub-total '.
2754 $total->{'total_amount'};
2755 push @sections, $tax_section if $taxtotal;
2757 unshift @total_items, $total;
2760 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2762 push @buf,['','-----------'];
2763 push @buf,[( $conf->exists('disable_previous_balance')
2765 : 'Total New Charges'
2767 $money_char. sprintf("%10.2f",$self->charged) ];
2773 $item = $conf->config('previous_balance-exclude_from_total')
2774 || 'Total New Charges'
2775 if $conf->exists('previous_balance-exclude_from_total');
2776 my $amount = $self->charged +
2777 ( $conf->exists('disable_previous_balance') ||
2778 $conf->exists('previous_balance-exclude_from_total')
2782 $total->{'total_item'} = &$embolden_function($item);
2783 $total->{'total_amount'} =
2784 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2785 if ( $multisection ) {
2786 if ( $adjust_section->{'sort_weight'} ) {
2787 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2788 sprintf("%.2f", ($self->billing_balance || 0) );
2790 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2791 sprintf('%.2f', $self->charged );
2794 push @total_items, $total;
2796 push @buf,['','-----------'];
2799 sprintf( '%10.2f', $amount )
2804 unless ( $conf->exists('disable_previous_balance') ) {
2805 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2808 my $credittotal = 0;
2809 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2812 $total->{'total_item'} = &$escape_function($credit->{'description'});
2813 $credittotal += $credit->{'amount'};
2814 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2815 $adjusttotal += $credit->{'amount'};
2816 if ( $multisection ) {
2817 my $money = $old_latex ? '' : $money_char;
2818 push @detail_items, {
2819 ext_description => [],
2822 description => &$escape_function($credit->{'description'}),
2823 amount => $money. $credit->{'amount'},
2825 section => $adjust_section,
2828 push @total_items, $total;
2832 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2835 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2836 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2840 my $paymenttotal = 0;
2841 foreach my $payment ( $self->_items_payments ) {
2843 $total->{'total_item'} = &$escape_function($payment->{'description'});
2844 $paymenttotal += $payment->{'amount'};
2845 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2846 $adjusttotal += $payment->{'amount'};
2847 if ( $multisection ) {
2848 my $money = $old_latex ? '' : $money_char;
2849 push @detail_items, {
2850 ext_description => [],
2853 description => &$escape_function($payment->{'description'}),
2854 amount => $money. $payment->{'amount'},
2856 section => $adjust_section,
2859 push @total_items, $total;
2861 push @buf, [ $payment->{'description'},
2862 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2865 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2867 if ( $multisection ) {
2868 $adjust_section->{'subtotal'} = $other_money_char.
2869 sprintf('%.2f', $adjusttotal);
2870 push @sections, $adjust_section
2871 unless $adjust_section->{sort_weight};
2876 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2877 $total->{'total_amount'} =
2878 &$embolden_function(
2879 $other_money_char. sprintf('%.2f', $summarypage
2881 $self->billing_balance
2882 : $self->owed + $pr_total
2885 if ( $multisection && !$adjust_section->{sort_weight} ) {
2886 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2887 $total->{'total_amount'};
2889 push @total_items, $total;
2891 push @buf,['','-----------'];
2892 push @buf,[$self->balance_due_msg, $money_char.
2893 sprintf("%10.2f", $balance_due ) ];
2897 if ( $multisection ) {
2898 if ($conf->exists('svc_phone_sections')) {
2900 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2901 $total->{'total_amount'} =
2902 &$embolden_function(
2903 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2905 my $last_section = pop @sections;
2906 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2907 $total->{'total_amount'};
2908 push @sections, $last_section;
2910 push @sections, @$late_sections
2914 my @includelist = ();
2915 push @includelist, 'summary' if $summarypage;
2916 foreach my $include ( @includelist ) {
2918 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2921 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2923 @inc_src = $conf->config($inc_file, $agentnum);
2927 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2929 my $convert_map = $convert_maps{$format}{$include};
2931 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2932 s/--\@\]/$delimiters{$format}[1]/g;
2935 &$convert_map( $conf->config($inc_file, $agentnum) );
2939 my $inc_tt = new Text::Template (
2941 SOURCE => [ map "$_\n", @inc_src ],
2942 DELIMITERS => $delimiters{$format},
2943 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2945 unless ( $inc_tt->compile() ) {
2946 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2947 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2951 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2953 $invoice_data{$include} =~ s/\n+$//
2954 if ($format eq 'latex');
2959 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2960 /invoice_lines\((\d*)\)/;
2961 $invoice_lines += $1 || scalar(@buf);
2964 die "no invoice_lines() functions in template?"
2965 if ( $format eq 'template' && !$wasfunc );
2967 if ($format eq 'template') {
2969 if ( $invoice_lines ) {
2970 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2971 $invoice_data{'total_pages'}++
2972 if scalar(@buf) % $invoice_lines;
2975 #setup subroutine for the template
2976 sub FS::cust_bill::_template::invoice_lines {
2977 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2979 scalar(@FS::cust_bill::_template::buf)
2980 ? shift @FS::cust_bill::_template::buf
2989 push @collect, split("\n",
2990 $text_template->fill_in( HASH => \%invoice_data,
2991 PACKAGE => 'FS::cust_bill::_template'
2994 $FS::cust_bill::_template::page++;
2996 map "$_\n", @collect;
2998 warn "filling in template for invoice ". $self->invnum. "\n"
3000 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3003 $text_template->fill_in(HASH => \%invoice_data);
3007 # helper routine for generating date ranges
3008 sub _prior_month30s {
3011 [ 1, 2592000 ], # 0-30 days ago
3012 [ 2592000, 5184000 ], # 30-60 days ago
3013 [ 5184000, 7776000 ], # 60-90 days ago
3014 [ 7776000, 0 ], # 90+ days ago
3017 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3018 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3023 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3025 Returns an postscript invoice, as a scalar.
3027 Options can be passed as a hashref (recommended) or as a list of time, template
3028 and then any key/value pairs for any other options.
3030 I<time> an optional value used to control the printing of overdue messages. The
3031 default is now. It isn't the date of the invoice; that's the `_date' field.
3032 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3033 L<Time::Local> and L<Date::Parse> for conversion functions.
3035 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3042 my ($file, $lfile) = $self->print_latex(@_);
3043 my $ps = generate_ps($file);
3044 unlink($file.'.tex');
3050 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3052 Returns an PDF invoice, as a scalar.
3054 Options can be passed as a hashref (recommended) or as a list of time, template
3055 and then any key/value pairs for any other options.
3057 I<time> an optional value used to control the printing of overdue messages. The
3058 default is now. It isn't the date of the invoice; that's the `_date' field.
3059 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3060 L<Time::Local> and L<Date::Parse> for conversion functions.
3062 I<template>, if specified, is the name of a suffix for alternate invoices.
3064 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3071 my ($file, $lfile) = $self->print_latex(@_);
3072 my $pdf = generate_pdf($file);
3073 unlink($file.'.tex');
3079 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3081 Returns an HTML invoice, as a scalar.
3083 I<time> an optional value used to control the printing of overdue messages. The
3084 default is now. It isn't the date of the invoice; that's the `_date' field.
3085 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3086 L<Time::Local> and L<Date::Parse> for conversion functions.
3088 I<template>, if specified, is the name of a suffix for alternate invoices.
3090 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3092 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3093 when emailing the invoice as part of a multipart/related MIME email.
3101 %params = %{ shift() };
3103 $params{'time'} = shift;
3104 $params{'template'} = shift;
3105 $params{'cid'} = shift;
3108 $params{'format'} = 'html';
3110 $self->print_generic( %params );
3113 # quick subroutine for print_latex
3115 # There are ten characters that LaTeX treats as special characters, which
3116 # means that they do not simply typeset themselves:
3117 # # $ % & ~ _ ^ \ { }
3119 # TeX ignores blanks following an escaped character; if you want a blank (as
3120 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3124 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3125 $value =~ s/([<>])/\$$1\$/g;
3129 #utility methods for print_*
3131 sub _translate_old_latex_format {
3132 warn "_translate_old_latex_format called\n"
3139 if ( $line =~ /^%%Detail\s*$/ ) {
3141 push @template, q![@--!,
3142 q! foreach my $_tr_line (@detail_items) {!,
3143 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3144 q! $_tr_line->{'description'} .= !,
3145 q! "\\tabularnewline\n~~".!,
3146 q! join( "\\tabularnewline\n~~",!,
3147 q! @{$_tr_line->{'ext_description'}}!,
3151 while ( ( my $line_item_line = shift )
3152 !~ /^%%EndDetail\s*$/ ) {
3153 $line_item_line =~ s/'/\\'/g; # nice LTS
3154 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3155 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3156 push @template, " \$OUT .= '$line_item_line';";
3159 push @template, '}',
3162 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3164 push @template, '[@--',
3165 ' foreach my $_tr_line (@total_items) {';
3167 while ( ( my $total_item_line = shift )
3168 !~ /^%%EndTotalDetails\s*$/ ) {
3169 $total_item_line =~ s/'/\\'/g; # nice LTS
3170 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3171 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3172 push @template, " \$OUT .= '$total_item_line';";
3175 push @template, '}',
3179 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3180 push @template, $line;
3186 warn "$_\n" foreach @template;
3195 #check for an invoice-specific override
3196 return $self->invoice_terms if $self->invoice_terms;
3198 #check for a customer- specific override
3199 my $cust_main = $self->cust_main;
3200 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3202 #use configured default
3203 $conf->config('invoice_default_terms') || '';
3209 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3210 $duedate = $self->_date() + ( $1 * 86400 );
3217 $self->due_date ? time2str(shift, $self->due_date) : '';
3220 sub balance_due_msg {
3222 my $msg = 'Balance Due';
3223 return $msg unless $self->terms;
3224 if ( $self->due_date ) {
3225 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3226 } elsif ( $self->terms ) {
3227 $msg .= ' - '. $self->terms;
3232 sub balance_due_date {
3235 if ( $conf->exists('invoice_default_terms')
3236 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3237 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3242 =item invnum_date_pretty
3244 Returns a string with the invoice number and date, for example:
3245 "Invoice #54 (3/20/2008)"
3249 sub invnum_date_pretty {
3251 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3256 Returns a string with the date, for example: "3/20/2008"
3262 time2str($date_format, $self->_date);
3265 use vars qw(%pkg_category_cache);
3266 sub _items_sections {
3269 my $summarypage = shift;
3271 my $extra_sections = shift;
3275 my %late_subtotal = ();
3278 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3281 my $usage = $cust_bill_pkg->usage;
3283 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3284 next if ( $display->summary && $summarypage );
3286 my $section = $display->section;
3287 my $type = $display->type;
3289 $not_tax{$section} = 1
3290 unless $cust_bill_pkg->pkgnum == 0;
3292 if ( $display->post_total && !$summarypage ) {
3293 if (! $type || $type eq 'S') {
3294 $late_subtotal{$section} += $cust_bill_pkg->setup
3295 if $cust_bill_pkg->setup != 0;
3299 $late_subtotal{$section} += $cust_bill_pkg->recur
3300 if $cust_bill_pkg->recur != 0;
3303 if ($type && $type eq 'R') {
3304 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3305 if $cust_bill_pkg->recur != 0;
3308 if ($type && $type eq 'U') {
3309 $late_subtotal{$section} += $usage
3310 unless scalar(@$extra_sections);
3315 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3317 if (! $type || $type eq 'S') {
3318 $subtotal{$section} += $cust_bill_pkg->setup
3319 if $cust_bill_pkg->setup != 0;
3323 $subtotal{$section} += $cust_bill_pkg->recur
3324 if $cust_bill_pkg->recur != 0;
3327 if ($type && $type eq 'R') {
3328 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3329 if $cust_bill_pkg->recur != 0;
3332 if ($type && $type eq 'U') {
3333 $subtotal{$section} += $usage
3334 unless scalar(@$extra_sections);
3343 %pkg_category_cache = ();
3345 push @$late, map { { 'description' => &{$escape}($_),
3346 'subtotal' => $late_subtotal{$_},
3348 'sort_weight' => ( _pkg_category($_)
3349 ? _pkg_category($_)->weight
3352 ((_pkg_category($_) && _pkg_category($_)->condense)
3353 ? $self->_condense_section($format)
3357 sort _sectionsort keys %late_subtotal;
3360 if ( $summarypage ) {
3361 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3362 map { $_->categoryname } qsearch('pkg_category', {});
3363 push @sections, '' if exists($subtotal{''});
3365 @sections = keys %subtotal;
3368 my @early = map { { 'description' => &{$escape}($_),
3369 'subtotal' => $subtotal{$_},
3370 'summarized' => $not_tax{$_} ? '' : 'Y',
3371 'tax_section' => $not_tax{$_} ? '' : 'Y',
3372 'sort_weight' => ( _pkg_category($_)
3373 ? _pkg_category($_)->weight
3376 ((_pkg_category($_) && _pkg_category($_)->condense)
3377 ? $self->_condense_section($format)
3382 push @early, @$extra_sections if $extra_sections;
3384 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3388 #helper subs for above
3391 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3395 my $categoryname = shift;
3396 $pkg_category_cache{$categoryname} ||=
3397 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3400 my %condensed_format = (
3401 'label' => [ qw( Description Qty Amount ) ],
3403 sub { shift->{description} },
3404 sub { shift->{quantity} },
3405 sub { my($href, %opt) = @_;
3406 ($opt{dollar} || ''). $href->{amount};
3409 'align' => [ qw( l r r ) ],
3410 'span' => [ qw( 5 1 1 ) ], # unitprices?
3411 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3414 sub _condense_section {
3415 my ( $self, $format ) = ( shift, shift );
3417 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3418 qw( description_generator
3421 total_line_generator
3426 sub _condensed_generator_defaults {
3427 my ( $self, $format ) = ( shift, shift );
3428 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3437 sub _condensed_header_generator {
3438 my ( $self, $format ) = ( shift, shift );
3440 my ( $f, $prefix, $suffix, $separator, $column ) =
3441 _condensed_generator_defaults($format);
3443 if ($format eq 'latex') {
3444 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3445 $suffix = "\\\\\n\\hline";
3448 sub { my ($d,$a,$s,$w) = @_;
3449 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3451 } elsif ( $format eq 'html' ) {
3452 $prefix = '<th></th>';
3456 sub { my ($d,$a,$s,$w) = @_;
3457 return qq!<th align="$html_align{$a}">$d</th>!;
3465 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3467 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3470 $prefix. join($separator, @result). $suffix;
3475 sub _condensed_description_generator {
3476 my ( $self, $format ) = ( shift, shift );
3478 my ( $f, $prefix, $suffix, $separator, $column ) =
3479 _condensed_generator_defaults($format);
3481 my $money_char = '$';
3482 if ($format eq 'latex') {
3483 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3485 $separator = " & \n";
3487 sub { my ($d,$a,$s,$w) = @_;
3488 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3490 $money_char = '\\dollar';
3491 }elsif ( $format eq 'html' ) {
3492 $prefix = '"><td align="center"></td>';
3496 sub { my ($d,$a,$s,$w) = @_;
3497 return qq!<td align="$html_align{$a}">$d</td>!;
3499 #$money_char = $conf->config('money_char') || '$';
3500 $money_char = ''; # this is madness
3508 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3510 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3512 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3513 map { $f->{$_}->[$i] } qw(align span width)
3517 $prefix. join( $separator, @result ). $suffix;
3522 sub _condensed_total_generator {
3523 my ( $self, $format ) = ( shift, shift );
3525 my ( $f, $prefix, $suffix, $separator, $column ) =
3526 _condensed_generator_defaults($format);
3529 if ($format eq 'latex') {
3532 $separator = " & \n";
3534 sub { my ($d,$a,$s,$w) = @_;
3535 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3537 }elsif ( $format eq 'html' ) {
3541 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3543 sub { my ($d,$a,$s,$w) = @_;
3544 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3553 # my $r = &{$f->{fields}->[$i]}(@args);
3554 # $r .= ' Total' unless $i;
3556 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3558 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3559 map { $f->{$_}->[$i] } qw(align span width)
3563 $prefix. join( $separator, @result ). $suffix;
3568 =item total_line_generator FORMAT
3570 Returns a coderef used for generation of invoice total line items for this
3571 usage_class. FORMAT is either html or latex
3575 # should not be used: will have issues with hash element names (description vs
3576 # total_item and amount vs total_amount -- another array of functions?
3578 sub _condensed_total_line_generator {
3579 my ( $self, $format ) = ( shift, shift );
3581 my ( $f, $prefix, $suffix, $separator, $column ) =
3582 _condensed_generator_defaults($format);
3585 if ($format eq 'latex') {
3588 $separator = " & \n";
3590 sub { my ($d,$a,$s,$w) = @_;
3591 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3593 }elsif ( $format eq 'html' ) {
3597 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3599 sub { my ($d,$a,$s,$w) = @_;
3600 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3609 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3611 &{$column}( &{$f->{fields}->[$i]}(@args),
3612 map { $f->{$_}->[$i] } qw(align span width)
3616 $prefix. join( $separator, @result ). $suffix;
3621 #sub _items_extra_usage_sections {
3623 # my $escape = shift;
3625 # my %sections = ();
3627 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3628 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3630 # next unless $cust_bill_pkg->pkgnum > 0;
3632 # foreach my $section ( keys %usage_class ) {
3634 # my $usage = $cust_bill_pkg->usage($section);
3636 # next unless $usage && $usage > 0;
3638 # $sections{$section} ||= 0;
3639 # $sections{$section} += $usage;
3645 # map { { 'description' => &{$escape}($_),
3646 # 'subtotal' => $sections{$_},
3647 # 'summarized' => '',
3648 # 'tax_section' => '',
3651 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3655 sub _items_extra_usage_sections {
3664 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3665 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3666 next unless $cust_bill_pkg->pkgnum > 0;
3668 foreach my $classnum ( keys %usage_class ) {
3669 my $section = $usage_class{$classnum}->classname;
3670 $classnums{$section} = $classnum;
3672 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3673 my $amount = $detail->amount;
3674 next unless $amount && $amount > 0;
3676 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3677 $sections{$section}{amount} += $amount; #subtotal
3678 $sections{$section}{calls}++;
3679 $sections{$section}{duration} += $detail->duration;
3681 my $desc = $detail->regionname;
3682 my $description = $desc;
3683 $description = substr($desc, 0, 50). '...'
3684 if $format eq 'latex' && length($desc) > 50;
3686 $lines{$section}{$desc} ||= {
3687 description => &{$escape}($description),
3688 #pkgpart => $part_pkg->pkgpart,
3689 pkgnum => $cust_bill_pkg->pkgnum,
3694 #unit_amount => $cust_bill_pkg->unitrecur,
3695 quantity => $cust_bill_pkg->quantity,
3696 product_code => 'N/A',
3697 ext_description => [],
3700 $lines{$section}{$desc}{amount} += $amount;
3701 $lines{$section}{$desc}{calls}++;
3702 $lines{$section}{$desc}{duration} += $detail->duration;
3708 my %sectionmap = ();
3709 foreach (keys %sections) {
3710 my $usage_class = $usage_class{$classnums{$_}};
3711 $sectionmap{$_} = { 'description' => &{$escape}($_),
3712 'amount' => $sections{$_}{amount}, #subtotal
3713 'calls' => $sections{$_}{calls},
3714 'duration' => $sections{$_}{duration},
3716 'tax_section' => '',
3717 'sort_weight' => $usage_class->weight,
3718 ( $usage_class->format
3719 ? ( map { $_ => $usage_class->$_($format) }
3720 qw( description_generator header_generator total_generator total_line_generator )
3727 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3731 foreach my $section ( keys %lines ) {
3732 foreach my $line ( keys %{$lines{$section}} ) {
3733 my $l = $lines{$section}{$line};
3734 $l->{section} = $sectionmap{$section};
3735 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3736 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3741 return(\@sections, \@lines);
3745 sub _items_svc_phone_sections {
3754 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3755 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3757 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3758 next unless $cust_bill_pkg->pkgnum > 0;
3760 my @header = $cust_bill_pkg->details_header;
3761 next unless scalar(@header);
3763 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3765 my $phonenum = $detail->phonenum;
3766 next unless $phonenum;
3768 my $amount = $detail->amount;
3769 next unless $amount && $amount > 0;
3771 $sections{$phonenum} ||= { 'amount' => 0,
3774 'sort_weight' => -1,
3775 'phonenum' => $phonenum,
3777 $sections{$phonenum}{amount} += $amount; #subtotal
3778 $sections{$phonenum}{calls}++;
3779 $sections{$phonenum}{duration} += $detail->duration;
3781 my $desc = $detail->regionname;
3782 my $description = $desc;
3783 $description = substr($desc, 0, 50). '...'
3784 if $format eq 'latex' && length($desc) > 50;
3786 $lines{$phonenum}{$desc} ||= {
3787 description => &{$escape}($description),
3788 #pkgpart => $part_pkg->pkgpart,
3796 product_code => 'N/A',
3797 ext_description => [],
3800 $lines{$phonenum}{$desc}{amount} += $amount;
3801 $lines{$phonenum}{$desc}{calls}++;
3802 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3804 my $line = $usage_class{$detail->classnum}->classname;
3805 $sections{"$phonenum $line"} ||=
3809 'sort_weight' => $usage_class{$detail->classnum}->weight,
3810 'phonenum' => $phonenum,
3811 'header' => [ @header ],
3813 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3814 $sections{"$phonenum $line"}{calls}++;
3815 $sections{"$phonenum $line"}{duration} += $detail->duration;
3817 $lines{"$phonenum $line"}{$desc} ||= {
3818 description => &{$escape}($description),
3819 #pkgpart => $part_pkg->pkgpart,
3827 product_code => 'N/A',
3828 ext_description => [],
3831 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3832 $lines{"$phonenum $line"}{$desc}{calls}++;
3833 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3834 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3835 $detail->formatted('format' => $format);
3840 my %sectionmap = ();
3841 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3842 foreach ( keys %sections ) {
3843 my @header = @{ $sections{$_}{header} || [] };
3845 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
3846 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3847 my $usage_class = $summary ? $simple : $usage_simple;
3848 my $ending = $summary ? ' usage charges' : '';
3851 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
3853 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3854 'amount' => $sections{$_}{amount}, #subtotal
3855 'calls' => $sections{$_}{calls},
3856 'duration' => $sections{$_}{duration},
3858 'tax_section' => '',
3859 'phonenum' => $sections{$_}{phonenum},
3860 'sort_weight' => $sections{$_}{sort_weight},
3861 'post_total' => $summary, #inspire pagebreak
3863 ( map { $_ => $usage_class->$_($format, %gen_opt) }
3864 qw( description_generator
3867 total_line_generator
3874 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3875 $a->{sort_weight} <=> $b->{sort_weight}
3880 foreach my $section ( keys %lines ) {
3881 foreach my $line ( keys %{$lines{$section}} ) {
3882 my $l = $lines{$section}{$line};
3883 $l->{section} = $sectionmap{$section};
3884 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3885 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3890 return(\@sections, \@lines);
3897 #my @display = scalar(@_)
3899 # : qw( _items_previous _items_pkg );
3900 # #: qw( _items_pkg );
3901 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3902 my @display = qw( _items_previous _items_pkg );
3905 foreach my $display ( @display ) {
3906 push @b, $self->$display(@_);
3911 sub _items_previous {
3913 my $cust_main = $self->cust_main;
3914 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3916 foreach ( @pr_cust_bill ) {
3917 my $date = $conf->exists('invoice_show_prior_due_date')
3918 ? 'due '. $_->due_date2str($date_format)
3919 : time2str($date_format, $_->_date);
3921 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3922 #'pkgpart' => 'N/A',
3924 'amount' => sprintf("%.2f", $_->owed),
3930 # 'description' => 'Previous Balance',
3931 # #'pkgpart' => 'N/A',
3932 # 'pkgnum' => 'N/A',
3933 # 'amount' => sprintf("%10.2f", $pr_total ),
3934 # 'ext_description' => [ map {
3935 # "Invoice ". $_->invnum.
3936 # " (". time2str("%x",$_->_date). ") ".
3937 # sprintf("%10.2f", $_->owed)
3938 # } @pr_cust_bill ],
3946 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3947 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3948 if ($options{section} && $options{section}->{condensed}) {
3950 local $Storable::canonical = 1;
3951 foreach ( @items ) {
3953 delete $item->{ref};
3954 delete $item->{ext_description};
3955 my $key = freeze($item);
3956 $itemshash{$key} ||= 0;
3957 $itemshash{$key} ++; # += $item->{quantity};
3959 @items = sort { $a->{description} cmp $b->{description} }
3960 map { my $i = thaw($_);
3961 $i->{quantity} = $itemshash{$_};
3963 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3972 return 0 unless $a->itemdesc cmp $b->itemdesc;
3973 return -1 if $b->itemdesc eq 'Tax';
3974 return 1 if $a->itemdesc eq 'Tax';
3975 return -1 if $b->itemdesc eq 'Other surcharges';
3976 return 1 if $a->itemdesc eq 'Other surcharges';
3977 $a->itemdesc cmp $b->itemdesc;
3982 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3983 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3986 sub _items_cust_bill_pkg {
3988 my $cust_bill_pkg = shift;
3991 my $format = $opt{format} || '';
3992 my $escape_function = $opt{escape_function} || sub { shift };
3993 my $format_function = $opt{format_function} || '';
3994 my $unsquelched = $opt{unsquelched} || '';
3995 my $section = $opt{section}->{description} if $opt{section};
3996 my $summary_page = $opt{summary_page} || '';
3997 my $multilocation = $opt{multilocation} || '';
3998 my $multisection = $opt{multisection} || '';
4001 my ($s, $r, $u) = ( undef, undef, undef );
4002 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4005 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4006 if ( $_ && !$cust_bill_pkg->hidden ) {
4007 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4008 $_->{amount} =~ s/^\-0\.00$/0.00/;
4009 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4011 unless $_->{amount} == 0;
4016 foreach my $display ( grep { defined($section)
4017 ? $_->section eq $section
4020 #grep { !$_->summary || !$summary_page } # bunk!
4021 grep { !$_->summary || $multisection }
4022 $cust_bill_pkg->cust_bill_pkg_display
4026 my $type = $display->type;
4028 my $desc = $cust_bill_pkg->desc;
4029 $desc = substr($desc, 0, 50). '...'
4030 if $format eq 'latex' && length($desc) > 50;
4032 my %details_opt = ( 'format' => $format,
4033 'escape_function' => $escape_function,
4034 'format_function' => $format_function,
4037 if ( $cust_bill_pkg->pkgnum > 0 ) {
4039 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4041 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4043 my $description = $desc;
4044 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4047 unless ( $cust_pkg->part_pkg->hide_svc_detail
4048 || $cust_bill_pkg->hidden )
4050 push @d, map &{$escape_function}($_),
4051 $cust_pkg->h_labels_short($self->_date);
4052 if ( $multilocation ) {
4053 my $loc = $cust_pkg->location_label;
4054 $loc = substr($desc, 0, 50). '...'
4055 if $format eq 'latex' && length($loc) > 50;
4056 push @d, &{$escape_function}($loc);
4059 push @d, $cust_bill_pkg->details(%details_opt)
4060 if $cust_bill_pkg->recur == 0;
4062 if ( $cust_bill_pkg->hidden ) {
4063 $s->{amount} += $cust_bill_pkg->setup;
4064 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4065 push @{ $s->{ext_description} }, @d;
4068 description => $description,
4069 #pkgpart => $part_pkg->pkgpart,
4070 pkgnum => $cust_bill_pkg->pkgnum,
4071 amount => $cust_bill_pkg->setup,
4072 unit_amount => $cust_bill_pkg->unitsetup,
4073 quantity => $cust_bill_pkg->quantity,
4074 ext_description => \@d,
4080 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ) &&
4081 ( !$type || $type eq 'R' || $type eq 'U' )
4085 my $is_summary = $display->summary;
4086 my $description = ($is_summary && $type && $type eq 'U')
4087 ? "Usage charges" : $desc;
4089 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4090 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4091 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4096 #at least until cust_bill_pkg has "past" ranges in addition to
4097 #the "future" sdate/edate ones... see #3032
4098 my @dates = ( $self->_date );
4099 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4100 push @dates, $prev->sdate if $prev;
4102 unless ( $cust_pkg->part_pkg->hide_svc_detail
4103 || $cust_bill_pkg->itemdesc
4104 || $cust_bill_pkg->hidden
4105 || $is_summary && $type && $type eq 'U' )
4107 push @d, map &{$escape_function}($_),
4108 $cust_pkg->h_labels_short(@dates)
4109 #$cust_bill_pkg->edate,
4110 #$cust_bill_pkg->sdate)
4112 if ( $multilocation ) {
4113 my $loc = $cust_pkg->location_label;
4114 $loc = substr($desc, 0, 50). '...'
4115 if $format eq 'latex' && length($loc) > 50;
4116 push @d, &{$escape_function}($loc);
4120 push @d, $cust_bill_pkg->details(%details_opt)
4121 unless ($is_summary || $type && $type eq 'R');
4125 $amount = $cust_bill_pkg->recur;
4126 }elsif($type eq 'R') {
4127 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4128 }elsif($type eq 'U') {
4129 $amount = $cust_bill_pkg->usage;
4132 if ( !$type || $type eq 'R' ) {
4134 if ( $cust_bill_pkg->hidden ) {
4135 $r->{amount} += $amount;
4136 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4137 push @{ $r->{ext_description} }, @d;
4140 description => $description,
4141 #pkgpart => $part_pkg->pkgpart,
4142 pkgnum => $cust_bill_pkg->pkgnum,
4144 unit_amount => $cust_bill_pkg->unitrecur,
4145 quantity => $cust_bill_pkg->quantity,
4146 ext_description => \@d,
4150 } else { # $type eq 'U'
4152 if ( $cust_bill_pkg->hidden ) {
4153 $u->{amount} += $amount;
4154 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4155 push @{ $u->{ext_description} }, @d;
4158 description => $description,
4159 #pkgpart => $part_pkg->pkgpart,
4160 pkgnum => $cust_bill_pkg->pkgnum,
4162 unit_amount => $cust_bill_pkg->unitrecur,
4163 quantity => $cust_bill_pkg->quantity,
4164 ext_description => \@d,
4170 } # recurring or usage with recurring charge
4172 } else { #pkgnum tax or one-shot line item (??)
4174 if ( $cust_bill_pkg->setup != 0 ) {
4176 'description' => $desc,
4177 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4180 if ( $cust_bill_pkg->recur != 0 ) {
4182 'description' => "$desc (".
4183 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4184 time2str($date_format, $cust_bill_pkg->edate). ')',
4185 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4195 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4197 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4198 $_->{amount} =~ s/^\-0\.00$/0.00/;
4199 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4201 unless $_->{amount} == 0;
4209 sub _items_credits {
4210 my( $self, %opt ) = @_;
4211 my $trim_len = $opt{'trim_len'} || 60;
4215 foreach ( $self->cust_credited ) {
4217 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4219 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4220 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4221 $reason = " ($reason) " if $reason;
4224 #'description' => 'Credit ref\#'. $_->crednum.
4225 # " (". time2str("%x",$_->cust_credit->_date) .")".
4227 'description' => 'Credit applied '.
4228 time2str($date_format,$_->cust_credit->_date). $reason,
4229 'amount' => sprintf("%.2f",$_->amount),
4237 sub _items_payments {
4241 #get & print payments
4242 foreach ( $self->cust_bill_pay ) {
4244 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4247 'description' => "Payment received ".
4248 time2str($date_format,$_->cust_pay->_date ),
4249 'amount' => sprintf("%.2f", $_->amount )
4257 =item call_details [ OPTION => VALUE ... ]
4259 Returns an array of CSV strings representing the call details for this invoice
4260 The only option available is the boolean prepend_billed_number
4265 my ($self, %opt) = @_;
4267 my $format_function = sub { shift };
4269 if ($opt{prepend_billed_number}) {
4270 $format_function = sub {
4274 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4279 my @details = map { $_->details( 'format_function' => $format_function,
4280 'escape_function' => sub{ return() },
4284 $self->cust_bill_pkg;
4285 my $header = $details[0];
4286 ( $header, grep { $_ ne $header } @details );
4296 =item process_reprint
4300 sub process_reprint {
4301 process_re_X('print', @_);
4304 =item process_reemail
4308 sub process_reemail {
4309 process_re_X('email', @_);
4317 process_re_X('fax', @_);
4325 process_re_X('ftp', @_);
4332 sub process_respool {
4333 process_re_X('spool', @_);
4336 use Storable qw(thaw);
4340 my( $method, $job ) = ( shift, shift );
4341 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4343 my $param = thaw(decode_base64(shift));
4344 warn Dumper($param) if $DEBUG;
4355 my($method, $job, %param ) = @_;
4357 warn "re_X $method for job $job with param:\n".
4358 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4361 #some false laziness w/search/cust_bill.html
4363 my $orderby = 'ORDER BY cust_bill._date';
4365 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4367 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4369 my @cust_bill = qsearch( {
4370 #'select' => "cust_bill.*",
4371 'table' => 'cust_bill',
4372 'addl_from' => $addl_from,
4374 'extra_sql' => $extra_sql,
4375 'order_by' => $orderby,
4379 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4381 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4384 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4385 foreach my $cust_bill ( @cust_bill ) {
4386 $cust_bill->$method();
4388 if ( $job ) { #progressbar foo
4390 if ( time - $min_sec > $last ) {
4391 my $error = $job->update_statustext(
4392 int( 100 * $num / scalar(@cust_bill) )
4394 die $error if $error;
4405 =head1 CLASS METHODS
4411 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4416 my ($class, $start, $end) = @_;
4418 $class->paid_sql($start, $end). ' - '.
4419 $class->credited_sql($start, $end);
4424 Returns an SQL fragment to retreive the net amount (charged minus credited).
4429 my ($class, $start, $end) = @_;
4430 'charged - '. $class->credited_sql($start, $end);
4435 Returns an SQL fragment to retreive the amount paid against this invoice.
4440 my ($class, $start, $end) = @_;
4441 $start &&= "AND cust_bill_pay._date <= $start";
4442 $end &&= "AND cust_bill_pay._date > $end";
4443 $start = '' unless defined($start);
4444 $end = '' unless defined($end);
4445 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4446 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4451 Returns an SQL fragment to retreive the amount credited against this invoice.
4456 my ($class, $start, $end) = @_;
4457 $start &&= "AND cust_credit_bill._date <= $start";
4458 $end &&= "AND cust_credit_bill._date > $end";
4459 $start = '' unless defined($start);
4460 $end = '' unless defined($end);
4461 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4462 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4465 =item search_sql_where HASHREF
4467 Class method which returns an SQL WHERE fragment to search for parameters
4468 specified in HASHREF. Valid parameters are
4474 List reference of start date, end date, as UNIX timestamps.
4484 List reference of charged limits (exclusive).
4488 List reference of charged limits (exclusive).
4492 flag, return open invoices only
4496 flag, return net invoices only
4500 =item newest_percust
4504 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4508 sub search_sql_where {
4509 my($class, $param) = @_;
4511 warn "$me search_sql_where called with params: \n".
4512 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4518 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4519 push @search, "cust_main.agentnum = $1";
4523 if ( $param->{_date} ) {
4524 my($beginning, $ending) = @{$param->{_date}};
4526 push @search, "cust_bill._date >= $beginning",
4527 "cust_bill._date < $ending";
4531 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4532 push @search, "cust_bill.invnum >= $1";
4534 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4535 push @search, "cust_bill.invnum <= $1";
4539 if ( $param->{charged} ) {
4540 my @charged = ref($param->{charged})
4541 ? @{ $param->{charged} }
4542 : ($param->{charged});
4544 push @search, map { s/^charged/cust_bill.charged/; $_; }
4548 my $owed_sql = FS::cust_bill->owed_sql;
4551 if ( $param->{owed} ) {
4552 my @owed = ref($param->{owed})
4553 ? @{ $param->{owed} }
4555 push @search, map { s/^owed/$owed_sql/; $_; }
4560 push @search, "0 != $owed_sql"
4561 if $param->{'open'};
4562 push @search, '0 != '. FS::cust_bill->net_sql
4566 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4567 if $param->{'days'};
4570 if ( $param->{'newest_percust'} ) {
4572 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4573 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4575 my @newest_where = map { my $x = $_;
4576 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4579 grep ! /^cust_main./, @search;
4580 my $newest_where = scalar(@newest_where)
4581 ? ' AND '. join(' AND ', @newest_where)
4585 push @search, "cust_bill._date = (
4586 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4587 WHERE newest_cust_bill.custnum = cust_bill.custnum
4593 #agent virtualization
4594 my $curuser = $FS::CurrentUser::CurrentUser;
4595 if ( $curuser->username eq 'fs_queue'
4596 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4598 my $newuser = qsearchs('access_user', {
4599 'username' => $username,
4603 $curuser = $newuser;
4605 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4608 push @search, $curuser->agentnums_sql;
4610 join(' AND ', @search );
4622 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4623 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base