4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use Storable qw( freeze thaw );
16 use FS::UID qw( datasrc );
17 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
18 use FS::Record qw( qsearch qsearchs dbh );
19 use FS::cust_main_Mixin;
21 use FS::cust_statement;
22 use FS::cust_bill_pkg;
23 use FS::cust_bill_pkg_display;
24 use FS::cust_bill_pkg_detail;
28 use FS::cust_credit_bill;
30 use FS::cust_pay_batch;
31 use FS::cust_bill_event;
34 use FS::cust_bill_pay;
35 use FS::cust_bill_pay_batch;
36 use FS::part_bill_event;
39 use FS::cust_bill_batch;
41 @ISA = qw( FS::cust_main_Mixin FS::Record );
44 $me = '[FS::cust_bill]';
46 #ask FS::UID to run this stuff for us later
47 FS::UID->install_callback( sub {
49 $money_char = $conf->config('money_char') || '$';
50 $date_format = $conf->config('date_format') || '%x'; #/YY
51 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
52 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
57 FS::cust_bill - Object methods for cust_bill records
63 $record = new FS::cust_bill \%hash;
64 $record = new FS::cust_bill { 'column' => 'value' };
66 $error = $record->insert;
68 $error = $new_record->replace($old_record);
70 $error = $record->delete;
72 $error = $record->check;
74 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
76 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
78 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
80 @cust_pay_objects = $cust_bill->cust_pay;
82 $tax_amount = $record->tax;
84 @lines = $cust_bill->print_text;
85 @lines = $cust_bill->print_text $time;
89 An FS::cust_bill object represents an invoice; a declaration that a customer
90 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
91 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
92 following fields are currently supported:
98 =item invnum - primary key (assigned automatically for new invoices)
100 =item custnum - customer (see L<FS::cust_main>)
102 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
103 L<Time::Local> and L<Date::Parse> for conversion functions.
105 =item charged - amount of this invoice
107 =item invoice_terms - optional terms override for this specific invoice
111 Customer info at invoice generation time
115 =item previous_balance
117 =item billing_balance
125 =item printed - deprecated
133 =item closed - books closed flag, empty or `Y'
135 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
137 =item agent_invid - legacy invoice number
147 Creates a new invoice. To add the invoice to the database, see L<"insert">.
148 Invoices are normally created by calling the bill method of a customer object
149 (see L<FS::cust_main>).
153 sub table { 'cust_bill'; }
155 sub cust_linked { $_[0]->cust_main_custnum; }
156 sub cust_unlinked_msg {
158 "WARNING: can't find cust_main.custnum ". $self->custnum.
159 ' (cust_bill.invnum '. $self->invnum. ')';
164 Adds this invoice to the database ("Posts" the invoice). If there is an error,
165 returns the error, otherwise returns false.
171 warn "$me insert called\n" if $DEBUG;
173 local $SIG{HUP} = 'IGNORE';
174 local $SIG{INT} = 'IGNORE';
175 local $SIG{QUIT} = 'IGNORE';
176 local $SIG{TERM} = 'IGNORE';
177 local $SIG{TSTP} = 'IGNORE';
178 local $SIG{PIPE} = 'IGNORE';
180 my $oldAutoCommit = $FS::UID::AutoCommit;
181 local $FS::UID::AutoCommit = 0;
184 my $error = $self->SUPER::insert;
186 $dbh->rollback if $oldAutoCommit;
190 if ( $self->get('cust_bill_pkg') ) {
191 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
192 $cust_bill_pkg->invnum($self->invnum);
193 my $error = $cust_bill_pkg->insert;
195 $dbh->rollback if $oldAutoCommit;
196 return "can't create invoice line item: $error";
201 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
208 This method now works but you probably shouldn't use it. Instead, apply a
209 credit against the invoice.
211 Using this method to delete invoices outright is really, really bad. There
212 would be no record you ever posted this invoice, and there are no check to
213 make sure charged = 0 or that there are no associated cust_bill_pkg records.
215 Really, don't use it.
221 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
223 local $SIG{HUP} = 'IGNORE';
224 local $SIG{INT} = 'IGNORE';
225 local $SIG{QUIT} = 'IGNORE';
226 local $SIG{TERM} = 'IGNORE';
227 local $SIG{TSTP} = 'IGNORE';
228 local $SIG{PIPE} = 'IGNORE';
230 my $oldAutoCommit = $FS::UID::AutoCommit;
231 local $FS::UID::AutoCommit = 0;
234 foreach my $table (qw(
246 foreach my $linked ( $self->$table() ) {
247 my $error = $linked->delete;
249 $dbh->rollback if $oldAutoCommit;
256 my $error = $self->SUPER::delete(@_);
258 $dbh->rollback if $oldAutoCommit;
262 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
268 =item replace [ OLD_RECORD ]
270 You can, but probably shouldn't modify invoices...
272 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
273 supplied, replaces this record. If there is an error, returns the error,
274 otherwise returns false.
278 #replace can be inherited from Record.pm
280 # replace_check is now the preferred way to #implement replace data checks
281 # (so $object->replace() works without an argument)
284 my( $new, $old ) = ( shift, shift );
285 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
286 #return "Can't change _date!" unless $old->_date eq $new->_date;
287 return "Can't change _date" unless $old->_date == $new->_date;
288 return "Can't change charged" unless $old->charged == $new->charged
289 || $old->charged == 0;
296 Checks all fields to make sure this is a valid invoice. If there is an error,
297 returns the error, otherwise returns false. Called by the insert and replace
306 $self->ut_numbern('invnum')
307 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
308 || $self->ut_numbern('_date')
309 || $self->ut_money('charged')
310 || $self->ut_numbern('printed')
311 || $self->ut_enum('closed', [ '', 'Y' ])
312 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
313 || $self->ut_numbern('agent_invid') #varchar?
315 return $error if $error;
317 $self->_date(time) unless $self->_date;
319 $self->printed(0) if $self->printed eq '';
326 Returns the displayed invoice number for this invoice: agent_invid if
327 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
333 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
334 return $self->agent_invid;
336 return $self->invnum;
342 Returns a list consisting of the total previous balance for this customer,
343 followed by the previous outstanding invoices (as FS::cust_bill objects also).
350 my @cust_bill = sort { $a->_date <=> $b->_date }
351 grep { $_->owed != 0 && $_->_date < $self->_date }
352 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
354 foreach ( @cust_bill ) { $total += $_->owed; }
360 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
367 { 'table' => 'cust_bill_pkg',
368 'hashref' => { 'invnum' => $self->invnum },
369 'order_by' => 'ORDER BY billpkgnum',
374 =item cust_bill_pkg_pkgnum PKGNUM
376 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
381 sub cust_bill_pkg_pkgnum {
382 my( $self, $pkgnum ) = @_;
384 { 'table' => 'cust_bill_pkg',
385 'hashref' => { 'invnum' => $self->invnum,
388 'order_by' => 'ORDER BY billpkgnum',
395 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
402 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
403 $self->cust_bill_pkg;
405 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
410 Returns true if any of the packages (or their definitions) corresponding to the
411 line items for this invoice have the no_auto flag set.
417 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
420 =item open_cust_bill_pkg
422 Returns the open line items for this invoice.
424 Note that cust_bill_pkg with both setup and recur fees are returned as two
425 separate line items, each with only one fee.
429 # modeled after cust_main::open_cust_bill
430 sub open_cust_bill_pkg {
433 # grep { $_->owed > 0 } $self->cust_bill_pkg
435 my %other = ( 'recur' => 'setup',
436 'setup' => 'recur', );
438 foreach my $field ( qw( recur setup )) {
439 push @open, map { $_->set( $other{$field}, 0 ); $_; }
440 grep { $_->owed($field) > 0 }
441 $self->cust_bill_pkg;
447 =item cust_bill_event
449 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
453 sub cust_bill_event {
455 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
458 =item num_cust_bill_event
460 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
464 sub num_cust_bill_event {
467 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
468 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
469 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
470 $sth->fetchrow_arrayref->[0];
475 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
479 #false laziness w/cust_pkg.pm
483 'table' => 'cust_event',
484 'addl_from' => 'JOIN part_event USING ( eventpart )',
485 'hashref' => { 'tablenum' => $self->invnum },
486 'extra_sql' => " AND eventtable = 'cust_bill' ",
492 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
496 #false laziness w/cust_pkg.pm
500 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
501 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
502 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
503 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
504 $sth->fetchrow_arrayref->[0];
509 Returns the customer (see L<FS::cust_main>) for this invoice.
515 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
518 =item cust_suspend_if_balance_over AMOUNT
520 Suspends the customer associated with this invoice if the total amount owed on
521 this invoice and all older invoices is greater than the specified amount.
523 Returns a list: an empty list on success or a list of errors.
527 sub cust_suspend_if_balance_over {
528 my( $self, $amount ) = ( shift, shift );
529 my $cust_main = $self->cust_main;
530 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
533 $cust_main->suspend(@_);
539 Depreciated. See the cust_credited method.
541 #Returns a list consisting of the total previous credited (see
542 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
543 #outstanding credits (FS::cust_credit objects).
549 croak "FS::cust_bill->cust_credit depreciated; see ".
550 "FS::cust_bill->cust_credit_bill";
553 #my @cust_credit = sort { $a->_date <=> $b->_date }
554 # grep { $_->credited != 0 && $_->_date < $self->_date }
555 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
557 #foreach (@cust_credit) { $total += $_->credited; }
558 #$total, @cust_credit;
563 Depreciated. See the cust_bill_pay method.
565 #Returns all payments (see L<FS::cust_pay>) for this invoice.
571 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
573 #sort { $a->_date <=> $b->_date }
574 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
580 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
583 sub cust_bill_pay_batch {
585 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
590 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
596 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
597 sort { $a->_date <=> $b->_date }
598 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
603 =item cust_credit_bill
605 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
611 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
612 sort { $a->_date <=> $b->_date }
613 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
617 sub cust_credit_bill {
618 shift->cust_credited(@_);
621 =item cust_bill_pay_pkgnum PKGNUM
623 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
624 with matching pkgnum.
628 sub cust_bill_pay_pkgnum {
629 my( $self, $pkgnum ) = @_;
630 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
631 sort { $a->_date <=> $b->_date }
632 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
638 =item cust_credited_pkgnum PKGNUM
640 =item cust_credit_bill_pkgnum PKGNUM
642 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
643 with matching pkgnum.
647 sub cust_credited_pkgnum {
648 my( $self, $pkgnum ) = @_;
649 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
650 sort { $a->_date <=> $b->_date }
651 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
657 sub cust_credit_bill_pkgnum {
658 shift->cust_credited_pkgnum(@_);
663 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
670 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
672 foreach (@taxlines) { $total += $_->setup; }
678 Returns the amount owed (still outstanding) on this invoice, which is charged
679 minus all payment applications (see L<FS::cust_bill_pay>) and credit
680 applications (see L<FS::cust_credit_bill>).
686 my $balance = $self->charged;
687 $balance -= $_->amount foreach ( $self->cust_bill_pay );
688 $balance -= $_->amount foreach ( $self->cust_credited );
689 $balance = sprintf( "%.2f", $balance);
690 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
695 my( $self, $pkgnum ) = @_;
697 #my $balance = $self->charged;
699 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
701 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
702 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
704 $balance = sprintf( "%.2f", $balance);
705 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
709 =item apply_payments_and_credits [ OPTION => VALUE ... ]
711 Applies unapplied payments and credits to this invoice.
713 A hash of optional arguments may be passed. Currently "manual" is supported.
714 If true, a payment receipt is sent instead of a statement when
715 'payment_receipt_email' configuration option is set.
717 If there is an error, returns the error, otherwise returns false.
721 sub apply_payments_and_credits {
722 my( $self, %options ) = @_;
724 local $SIG{HUP} = 'IGNORE';
725 local $SIG{INT} = 'IGNORE';
726 local $SIG{QUIT} = 'IGNORE';
727 local $SIG{TERM} = 'IGNORE';
728 local $SIG{TSTP} = 'IGNORE';
729 local $SIG{PIPE} = 'IGNORE';
731 my $oldAutoCommit = $FS::UID::AutoCommit;
732 local $FS::UID::AutoCommit = 0;
735 $self->select_for_update; #mutex
737 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
738 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
740 if ( $conf->exists('pkg-balances') ) {
741 # limit @payments & @credits to those w/ a pkgnum grepped from $self
742 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
743 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
744 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
747 while ( $self->owed > 0 and ( @payments || @credits ) ) {
750 if ( @payments && @credits ) {
752 #decide which goes first by weight of top (unapplied) line item
754 my @open_lineitems = $self->open_cust_bill_pkg;
757 max( map { $_->part_pkg->pay_weight || 0 }
762 my $max_credit_weight =
763 max( map { $_->part_pkg->credit_weight || 0 }
769 #if both are the same... payments first? it has to be something
770 if ( $max_pay_weight >= $max_credit_weight ) {
776 } elsif ( @payments ) {
778 } elsif ( @credits ) {
781 die "guru meditation #12 and 35";
785 if ( $app eq 'pay' ) {
787 my $payment = shift @payments;
788 $unapp_amount = $payment->unapplied;
789 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
790 $app->pkgnum( $payment->pkgnum )
791 if $conf->exists('pkg-balances') && $payment->pkgnum;
793 } elsif ( $app eq 'credit' ) {
795 my $credit = shift @credits;
796 $unapp_amount = $credit->credited;
797 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
798 $app->pkgnum( $credit->pkgnum )
799 if $conf->exists('pkg-balances') && $credit->pkgnum;
802 die "guru meditation #12 and 35";
806 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
807 warn "owed_pkgnum ". $app->pkgnum;
808 $owed = $self->owed_pkgnum($app->pkgnum);
812 next unless $owed > 0;
814 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
815 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
817 $app->invnum( $self->invnum );
819 my $error = $app->insert(%options);
821 $dbh->rollback if $oldAutoCommit;
822 return "Error inserting ". $app->table. " record: $error";
824 die $error if $error;
828 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
833 =item generate_email OPTION => VALUE ...
841 sender address, required
845 alternate template name, optional
849 text attachment arrayref, optional
853 email subject, optional
857 notice name instead of "Invoice", optional
861 Returns an argument list to be passed to L<FS::Misc::send_email>.
872 my $me = '[FS::cust_bill::generate_email]';
875 'from' => $args{'from'},
876 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
880 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
881 'template' => $args{'template'},
882 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
885 my $cust_main = $self->cust_main;
887 if (ref($args{'to'}) eq 'ARRAY') {
888 $return{'to'} = $args{'to'};
890 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
891 $cust_main->invoicing_list
895 if ( $conf->exists('invoice_html') ) {
897 warn "$me creating HTML/text multipart message"
900 $return{'nobody'} = 1;
902 my $alternative = build MIME::Entity
903 'Type' => 'multipart/alternative',
904 'Encoding' => '7bit',
905 'Disposition' => 'inline'
909 if ( $conf->exists('invoice_email_pdf')
910 and scalar($conf->config('invoice_email_pdf_note')) ) {
912 warn "$me using 'invoice_email_pdf_note' in multipart message"
914 $data = [ map { $_ . "\n" }
915 $conf->config('invoice_email_pdf_note')
920 warn "$me not using 'invoice_email_pdf_note' in multipart message"
922 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
923 $data = $args{'print_text'};
925 $data = [ $self->print_text(\%opt) ];
930 $alternative->attach(
931 'Type' => 'text/plain',
932 #'Encoding' => 'quoted-printable',
933 'Encoding' => '7bit',
935 'Disposition' => 'inline',
938 $args{'from'} =~ /\@([\w\.\-]+)/;
939 my $from = $1 || 'example.com';
940 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
943 my $agentnum = $cust_main->agentnum;
944 if ( defined($args{'template'}) && length($args{'template'})
945 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
948 $logo = 'logo_'. $args{'template'}. '.png';
952 my $image_data = $conf->config_binary( $logo, $agentnum);
954 my $image = build MIME::Entity
955 'Type' => 'image/png',
956 'Encoding' => 'base64',
957 'Data' => $image_data,
958 'Filename' => 'logo.png',
959 'Content-ID' => "<$content_id>",
962 $alternative->attach(
963 'Type' => 'text/html',
964 'Encoding' => 'quoted-printable',
965 'Data' => [ '<html>',
968 ' '. encode_entities($return{'subject'}),
971 ' <body bgcolor="#e8e8e8">',
972 $self->print_html({ 'cid'=>$content_id, %opt }),
976 'Disposition' => 'inline',
977 #'Filename' => 'invoice.pdf',
981 if ( $cust_main->email_csv_cdr ) {
983 push @otherparts, build MIME::Entity
984 'Type' => 'text/csv',
985 'Encoding' => '7bit',
986 'Data' => [ map { "$_\n" }
987 $self->call_details('prepend_billed_number' => 1)
989 'Disposition' => 'attachment',
990 'Filename' => 'usage-'. $self->invnum. '.csv',
995 if ( $conf->exists('invoice_email_pdf') ) {
1000 # multipart/alternative
1006 my $related = build MIME::Entity 'Type' => 'multipart/related',
1007 'Encoding' => '7bit';
1009 #false laziness w/Misc::send_email
1010 $related->head->replace('Content-type',
1011 $related->mime_type.
1012 '; boundary="'. $related->head->multipart_boundary. '"'.
1013 '; type=multipart/alternative'
1016 $related->add_part($alternative);
1018 $related->add_part($image);
1020 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1022 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1026 #no other attachment:
1028 # multipart/alternative
1033 $return{'content-type'} = 'multipart/related';
1034 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1035 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1036 #$return{'disposition'} = 'inline';
1042 if ( $conf->exists('invoice_email_pdf') ) {
1043 warn "$me creating PDF attachment"
1046 #mime parts arguments a la MIME::Entity->build().
1047 $return{'mimeparts'} = [
1048 { $self->mimebuild_pdf(\%opt) }
1052 if ( $conf->exists('invoice_email_pdf')
1053 and scalar($conf->config('invoice_email_pdf_note')) ) {
1055 warn "$me using 'invoice_email_pdf_note'"
1057 $return{'body'} = [ map { $_ . "\n" }
1058 $conf->config('invoice_email_pdf_note')
1063 warn "$me not using 'invoice_email_pdf_note'"
1065 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1066 $return{'body'} = $args{'print_text'};
1068 $return{'body'} = [ $self->print_text(\%opt) ];
1081 Returns a list suitable for passing to MIME::Entity->build(), representing
1082 this invoice as PDF attachment.
1089 'Type' => 'application/pdf',
1090 'Encoding' => 'base64',
1091 'Data' => [ $self->print_pdf(@_) ],
1092 'Disposition' => 'attachment',
1093 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1097 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1099 Sends this invoice to the destinations configured for this customer: sends
1100 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1102 Options can be passed as a hashref (recommended) or as a list of up to
1103 four values for templatename, agentnum, invoice_from and amount.
1105 I<template>, if specified, is the name of a suffix for alternate invoices.
1107 I<agentnum>, if specified, means that this invoice will only be sent for customers
1108 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1109 single agent) or an arrayref of agentnums.
1111 I<invoice_from>, if specified, overrides the default email invoice From: address.
1113 I<amount>, if specified, only sends the invoice if the total amount owed on this
1114 invoice and all older invoices is greater than the specified amount.
1116 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1120 sub queueable_send {
1123 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1124 or die "invalid invoice number: " . $opt{invnum};
1126 my @args = ( $opt{template}, $opt{agentnum} );
1127 push @args, $opt{invoice_from}
1128 if exists($opt{invoice_from}) && $opt{invoice_from};
1130 my $error = $self->send( @args );
1131 die $error if $error;
1138 my( $template, $invoice_from, $notice_name );
1140 my $balance_over = 0;
1144 $template = $opt->{'template'} || '';
1145 if ( $agentnums = $opt->{'agentnum'} ) {
1146 $agentnums = [ $agentnums ] unless ref($agentnums);
1148 $invoice_from = $opt->{'invoice_from'};
1149 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1150 $notice_name = $opt->{'notice_name'};
1152 $template = scalar(@_) ? shift : '';
1153 if ( scalar(@_) && $_[0] ) {
1154 $agentnums = ref($_[0]) ? shift : [ shift ];
1156 $invoice_from = shift if scalar(@_);
1157 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1160 return 'N/A' unless ! $agentnums
1161 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1164 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1166 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1167 $conf->config('invoice_from', $self->cust_main->agentnum );
1170 'template' => $template,
1171 'invoice_from' => $invoice_from,
1172 'notice_name' => ( $notice_name || 'Invoice' ),
1175 my @invoicing_list = $self->cust_main->invoicing_list;
1177 #$self->email_invoice(\%opt)
1179 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1181 #$self->print_invoice(\%opt)
1183 if grep { $_ eq 'POST' } @invoicing_list; #postal
1185 $self->fax_invoice(\%opt)
1186 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1192 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1194 Emails this invoice.
1196 Options can be passed as a hashref (recommended) or as a list of up to
1197 two values for templatename and invoice_from.
1199 I<template>, if specified, is the name of a suffix for alternate invoices.
1201 I<invoice_from>, if specified, overrides the default email invoice From: address.
1203 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1207 sub queueable_email {
1210 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1211 or die "invalid invoice number: " . $opt{invnum};
1213 my @args = ( $opt{template} );
1214 push @args, $opt{invoice_from}
1215 if exists($opt{invoice_from}) && $opt{invoice_from};
1217 my $error = $self->email( @args );
1218 die $error if $error;
1222 #sub email_invoice {
1226 my( $template, $invoice_from, $notice_name );
1229 $template = $opt->{'template'} || '';
1230 $invoice_from = $opt->{'invoice_from'};
1231 $notice_name = $opt->{'notice_name'} || 'Invoice';
1233 $template = scalar(@_) ? shift : '';
1234 $invoice_from = shift if scalar(@_);
1235 $notice_name = 'Invoice';
1238 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1239 $conf->config('invoice_from', $self->cust_main->agentnum );
1241 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1242 $self->cust_main->invoicing_list;
1244 #better to notify this person than silence
1245 @invoicing_list = ($invoice_from) unless @invoicing_list;
1247 my $subject = $self->email_subject($template);
1249 my $error = send_email(
1250 $self->generate_email(
1251 'from' => $invoice_from,
1252 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1253 'subject' => $subject,
1254 'template' => $template,
1255 'notice_name' => $notice_name,
1258 die "can't email invoice: $error\n" if $error;
1259 #die "$error\n" if $error;
1266 #my $template = scalar(@_) ? shift : '';
1269 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1272 my $cust_main = $self->cust_main;
1273 my $name = $cust_main->name;
1274 my $name_short = $cust_main->name_short;
1275 my $invoice_number = $self->invnum;
1276 my $invoice_date = $self->_date_pretty;
1278 eval qq("$subject");
1281 =item lpr_data HASHREF | [ TEMPLATE ]
1283 Returns the postscript or plaintext for this invoice as an arrayref.
1285 Options can be passed as a hashref (recommended) or as a single optional value
1288 I<template>, if specified, is the name of a suffix for alternate invoices.
1290 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1296 my( $template, $notice_name );
1299 $template = $opt->{'template'} || '';
1300 $notice_name = $opt->{'notice_name'} || 'Invoice';
1302 $template = scalar(@_) ? shift : '';
1303 $notice_name = 'Invoice';
1307 'template' => $template,
1308 'notice_name' => $notice_name,
1311 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1312 [ $self->$method( \%opt ) ];
1315 =item print HASHREF | [ TEMPLATE ]
1317 Prints this invoice.
1319 Options can be passed as a hashref (recommended) or as a single optional
1322 I<template>, if specified, is the name of a suffix for alternate invoices.
1324 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1328 #sub print_invoice {
1331 my( $template, $notice_name );
1334 $template = $opt->{'template'} || '';
1335 $notice_name = $opt->{'notice_name'} || 'Invoice';
1337 $template = scalar(@_) ? shift : '';
1338 $notice_name = 'Invoice';
1342 'template' => $template,
1343 'notice_name' => $notice_name,
1346 if($conf->exists('invoice_print_pdf')) {
1347 # Add the invoice to the current batch.
1348 $self->batch_invoice(\%opt);
1351 do_print $self->lpr_data(\%opt);
1355 =item fax_invoice HASHREF | [ TEMPLATE ]
1359 Options can be passed as a hashref (recommended) or as a single optional
1362 I<template>, if specified, is the name of a suffix for alternate invoices.
1364 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1370 my( $template, $notice_name );
1373 $template = $opt->{'template'} || '';
1374 $notice_name = $opt->{'notice_name'} || 'Invoice';
1376 $template = scalar(@_) ? shift : '';
1377 $notice_name = 'Invoice';
1380 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1381 unless $conf->exists('invoice_latex');
1383 my $dialstring = $self->cust_main->getfield('fax');
1387 'template' => $template,
1388 'notice_name' => $notice_name,
1391 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1392 'dialstring' => $dialstring,
1394 die $error if $error;
1398 =item batch_invoice [ HASHREF ]
1400 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1401 isn't an open batch, one will be created.
1406 my ($self, $opt) = @_;
1407 my $batch = FS::bill_batch->get_open_batch;
1408 my $cust_bill_batch = FS::cust_bill_batch->new({
1409 batchnum => $batch->batchnum,
1410 invnum => $self->invnum,
1412 return $cust_bill_batch->insert($opt);
1415 =item ftp_invoice [ TEMPLATENAME ]
1417 Sends this invoice data via FTP.
1419 TEMPLATENAME is unused?
1425 my $template = scalar(@_) ? shift : '';
1428 'protocol' => 'ftp',
1429 'server' => $conf->config('cust_bill-ftpserver'),
1430 'username' => $conf->config('cust_bill-ftpusername'),
1431 'password' => $conf->config('cust_bill-ftppassword'),
1432 'dir' => $conf->config('cust_bill-ftpdir'),
1433 'format' => $conf->config('cust_bill-ftpformat'),
1437 =item spool_invoice [ TEMPLATENAME ]
1439 Spools this invoice data (see L<FS::spool_csv>)
1441 TEMPLATENAME is unused?
1447 my $template = scalar(@_) ? shift : '';
1450 'format' => $conf->config('cust_bill-spoolformat'),
1451 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1455 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1457 Like B<send>, but only sends the invoice if it is the newest open invoice for
1462 sub send_if_newest {
1467 grep { $_->owed > 0 }
1468 qsearch('cust_bill', {
1469 'custnum' => $self->custnum,
1470 #'_date' => { op=>'>', value=>$self->_date },
1471 'invnum' => { op=>'>', value=>$self->invnum },
1478 =item send_csv OPTION => VALUE, ...
1480 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1484 protocol - currently only "ftp"
1490 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1491 and YYMMDDHHMMSS is a timestamp.
1493 See L</print_csv> for a description of the output format.
1498 my($self, %opt) = @_;
1502 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1503 mkdir $spooldir, 0700 unless -d $spooldir;
1505 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1506 my $file = "$spooldir/$tracctnum.csv";
1508 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1510 open(CSV, ">$file") or die "can't open $file: $!";
1518 if ( $opt{protocol} eq 'ftp' ) {
1519 eval "use Net::FTP;";
1521 $net = Net::FTP->new($opt{server}) or die @$;
1523 die "unknown protocol: $opt{protocol}";
1526 $net->login( $opt{username}, $opt{password} )
1527 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1529 $net->binary or die "can't set binary mode";
1531 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1533 $net->put($file) or die "can't put $file: $!";
1543 Spools CSV invoice data.
1549 =item format - 'default' or 'billco'
1551 =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>).
1553 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1555 =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.
1562 my($self, %opt) = @_;
1564 my $cust_main = $self->cust_main;
1566 if ( $opt{'dest'} ) {
1567 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1568 $cust_main->invoicing_list;
1569 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1570 || ! keys %invoicing_list;
1573 if ( $opt{'balanceover'} ) {
1575 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1578 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1579 mkdir $spooldir, 0700 unless -d $spooldir;
1581 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1585 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1586 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1589 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1591 open(CSV, ">>$file") or die "can't open $file: $!";
1592 flock(CSV, LOCK_EX);
1597 if ( lc($opt{'format'}) eq 'billco' ) {
1599 flock(CSV, LOCK_UN);
1604 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1607 open(CSV,">>$file") or die "can't open $file: $!";
1608 flock(CSV, LOCK_EX);
1614 flock(CSV, LOCK_UN);
1621 =item print_csv OPTION => VALUE, ...
1623 Returns CSV data for this invoice.
1627 format - 'default' or 'billco'
1629 Returns a list consisting of two scalars. The first is a single line of CSV
1630 header information for this invoice. The second is one or more lines of CSV
1631 detail information for this invoice.
1633 If I<format> is not specified or "default", the fields of the CSV file are as
1636 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1640 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1642 B<record_type> is C<cust_bill> for the initial header line only. The
1643 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1644 fields are filled in.
1646 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1647 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1650 =item invnum - invoice number
1652 =item custnum - customer number
1654 =item _date - invoice date
1656 =item charged - total invoice amount
1658 =item first - customer first name
1660 =item last - customer first name
1662 =item company - company name
1664 =item address1 - address line 1
1666 =item address2 - address line 1
1676 =item pkg - line item description
1678 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1680 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1682 =item sdate - start date for recurring fee
1684 =item edate - end date for recurring fee
1688 If I<format> is "billco", the fields of the header CSV file are as follows:
1690 +-------------------------------------------------------------------+
1691 | FORMAT HEADER FILE |
1692 |-------------------------------------------------------------------|
1693 | Field | Description | Name | Type | Width |
1694 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1695 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1696 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1697 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1698 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1699 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1700 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1701 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1702 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1703 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1704 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1705 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1706 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1707 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1708 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1709 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1710 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1711 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1712 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1713 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1714 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1715 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1716 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1717 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1718 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1719 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1720 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1721 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1722 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1723 +-------+-------------------------------+------------+------+-------+
1725 If I<format> is "billco", the fields of the detail CSV file are as follows:
1727 FORMAT FOR DETAIL FILE
1729 Field | Description | Name | Type | Width
1730 1 | N/A-Leave Empty | RC | CHAR | 2
1731 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1732 3 | Account Number | TRACCTNUM | CHAR | 15
1733 4 | Invoice Number | TRINVOICE | CHAR | 15
1734 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1735 6 | Transaction Detail | DETAILS | CHAR | 100
1736 7 | Amount | AMT | NUM* | 9
1737 8 | Line Format Control** | LNCTRL | CHAR | 2
1738 9 | Grouping Code | GROUP | CHAR | 2
1739 10 | User Defined | ACCT CODE | CHAR | 15
1744 my($self, %opt) = @_;
1746 eval "use Text::CSV_XS";
1749 my $cust_main = $self->cust_main;
1751 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1753 if ( lc($opt{'format'}) eq 'billco' ) {
1756 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1758 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1760 my( $previous_balance, @unused ) = $self->previous; #previous balance
1762 my $pmt_cr_applied = 0;
1763 $pmt_cr_applied += $_->{'amount'}
1764 foreach ( $self->_items_payments, $self->_items_credits ) ;
1766 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1769 '', # 1 | N/A-Leave Empty CHAR 2
1770 '', # 2 | N/A-Leave Empty CHAR 15
1771 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1772 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1773 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1774 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1775 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1776 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1777 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1778 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1779 '', # 10 | Ancillary Billing Information CHAR 30
1780 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1781 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1784 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1787 $duedate, # 14 | Bill Due Date CHAR 10
1789 $previous_balance, # 15 | Previous Balance NUM* 9
1790 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1791 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1792 $totaldue, # 18 | Total Amt Due NUM* 9
1793 $totaldue, # 19 | Total Amt Due NUM* 9
1794 '', # 20 | 30 Day Aging NUM* 9
1795 '', # 21 | 60 Day Aging NUM* 9
1796 '', # 22 | 90 Day Aging NUM* 9
1797 'N', # 23 | Y/N CHAR 1
1798 '', # 24 | Remittance automation CHAR 100
1799 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1800 $self->custnum, # 26 | Customer Reference Number CHAR 15
1801 '0', # 27 | Federal Tax*** NUM* 9
1802 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1803 '0', # 29 | Other Taxes & Fees*** NUM* 9
1812 time2str("%x", $self->_date),
1813 sprintf("%.2f", $self->charged),
1814 ( map { $cust_main->getfield($_) }
1815 qw( first last company address1 address2 city state zip country ) ),
1817 ) or die "can't create csv";
1820 my $header = $csv->string. "\n";
1823 if ( lc($opt{'format'}) eq 'billco' ) {
1826 foreach my $item ( $self->_items_pkg ) {
1829 '', # 1 | N/A-Leave Empty CHAR 2
1830 '', # 2 | N/A-Leave Empty CHAR 15
1831 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1832 $self->invnum, # 4 | Invoice Number CHAR 15
1833 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1834 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1835 $item->{'amount'}, # 7 | Amount NUM* 9
1836 '', # 8 | Line Format Control** CHAR 2
1837 '', # 9 | Grouping Code CHAR 2
1838 '', # 10 | User Defined CHAR 15
1841 $detail .= $csv->string. "\n";
1847 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1849 my($pkg, $setup, $recur, $sdate, $edate);
1850 if ( $cust_bill_pkg->pkgnum ) {
1852 ($pkg, $setup, $recur, $sdate, $edate) = (
1853 $cust_bill_pkg->part_pkg->pkg,
1854 ( $cust_bill_pkg->setup != 0
1855 ? sprintf("%.2f", $cust_bill_pkg->setup )
1857 ( $cust_bill_pkg->recur != 0
1858 ? sprintf("%.2f", $cust_bill_pkg->recur )
1860 ( $cust_bill_pkg->sdate
1861 ? time2str("%x", $cust_bill_pkg->sdate)
1863 ($cust_bill_pkg->edate
1864 ?time2str("%x", $cust_bill_pkg->edate)
1868 } else { #pkgnum tax
1869 next unless $cust_bill_pkg->setup != 0;
1870 $pkg = $cust_bill_pkg->desc;
1871 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1872 ( $sdate, $edate ) = ( '', '' );
1878 ( map { '' } (1..11) ),
1879 ($pkg, $setup, $recur, $sdate, $edate)
1880 ) or die "can't create csv";
1882 $detail .= $csv->string. "\n";
1888 ( $header, $detail );
1894 Pays this invoice with a compliemntary payment. If there is an error,
1895 returns the error, otherwise returns false.
1901 my $cust_pay = new FS::cust_pay ( {
1902 'invnum' => $self->invnum,
1903 'paid' => $self->owed,
1906 'payinfo' => $self->cust_main->payinfo,
1914 Attempts to pay this invoice with a credit card payment via a
1915 Business::OnlinePayment realtime gateway. See
1916 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1917 for supported processors.
1923 $self->realtime_bop( 'CC', @_ );
1928 Attempts to pay this invoice with an electronic check (ACH) payment via a
1929 Business::OnlinePayment realtime gateway. See
1930 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1931 for supported processors.
1937 $self->realtime_bop( 'ECHECK', @_ );
1942 Attempts to pay this invoice with phone bill (LEC) payment via a
1943 Business::OnlinePayment realtime gateway. See
1944 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1945 for supported processors.
1951 $self->realtime_bop( 'LEC', @_ );
1955 my( $self, $method ) = @_;
1957 my $cust_main = $self->cust_main;
1958 my $balance = $cust_main->balance;
1959 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1960 $amount = sprintf("%.2f", $amount);
1961 return "not run (balance $balance)" unless $amount > 0;
1963 my $description = 'Internet Services';
1964 if ( $conf->exists('business-onlinepayment-description') ) {
1965 my $dtempl = $conf->config('business-onlinepayment-description');
1967 my $agent_obj = $cust_main->agent
1968 or die "can't retreive agent for $cust_main (agentnum ".
1969 $cust_main->agentnum. ")";
1970 my $agent = $agent_obj->agent;
1971 my $pkgs = join(', ',
1972 map { $_->part_pkg->pkg }
1973 grep { $_->pkgnum } $self->cust_bill_pkg
1975 $description = eval qq("$dtempl");
1978 $cust_main->realtime_bop($method, $amount,
1979 'description' => $description,
1980 'invnum' => $self->invnum,
1981 #this didn't do what we want, it just calls apply_payments_and_credits
1983 'apply_to_invoice' => 1,
1985 #this changes application behavior: auto payments
1986 #triggered against a specific invoice are now applied
1987 #to that invoice instead of oldest open.
1993 =item batch_card OPTION => VALUE...
1995 Adds a payment for this invoice to the pending credit card batch (see
1996 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1997 runs the payment using a realtime gateway.
2002 my ($self, %options) = @_;
2003 my $cust_main = $self->cust_main;
2005 $options{invnum} = $self->invnum;
2007 $cust_main->batch_card(%options);
2010 sub _agent_template {
2012 $self->cust_main->agent_template;
2015 sub _agent_invoice_from {
2017 $self->cust_main->agent_invoice_from;
2020 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2022 Returns an text invoice, as a list of lines.
2024 Options can be passed as a hashref (recommended) or as a list of time, template
2025 and then any key/value pairs for any other options.
2027 I<time>, if specified, is used to control the printing of overdue messages. The
2028 default is now. It isn't the date of the invoice; that's the `_date' field.
2029 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2030 L<Time::Local> and L<Date::Parse> for conversion functions.
2032 I<template>, if specified, is the name of a suffix for alternate invoices.
2034 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2040 my( $today, $template, %opt );
2042 %opt = %{ shift() };
2043 $today = delete($opt{'time'}) || '';
2044 $template = delete($opt{template}) || '';
2046 ( $today, $template, %opt ) = @_;
2049 my %params = ( 'format' => 'template' );
2050 $params{'time'} = $today if $today;
2051 $params{'template'} = $template if $template;
2052 $params{$_} = $opt{$_}
2053 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2055 $self->print_generic( %params );
2058 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2060 Internal method - returns a filename of a filled-in LaTeX template for this
2061 invoice (Note: add ".tex" to get the actual filename), and a filename of
2062 an associated logo (with the .eps extension included).
2064 See print_ps and print_pdf for methods that return PostScript and PDF output.
2066 Options can be passed as a hashref (recommended) or as a list of time, template
2067 and then any key/value pairs for any other options.
2069 I<time>, if specified, is used to control the printing of overdue messages. The
2070 default is now. It isn't the date of the invoice; that's the `_date' field.
2071 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2072 L<Time::Local> and L<Date::Parse> for conversion functions.
2074 I<template>, if specified, is the name of a suffix for alternate invoices.
2076 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2082 my( $today, $template, %opt );
2084 %opt = %{ shift() };
2085 $today = delete($opt{'time'}) || '';
2086 $template = delete($opt{template}) || '';
2088 ( $today, $template, %opt ) = @_;
2091 my %params = ( 'format' => 'latex' );
2092 $params{'time'} = $today if $today;
2093 $params{'template'} = $template if $template;
2094 $params{$_} = $opt{$_}
2095 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2097 $template ||= $self->_agent_template;
2099 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2100 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2104 ) or die "can't open temp file: $!\n";
2106 my $agentnum = $self->cust_main->agentnum;
2108 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2109 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2110 or die "can't write temp file: $!\n";
2112 print $lh $conf->config_binary('logo.eps', $agentnum)
2113 or die "can't write temp file: $!\n";
2116 $params{'logo_file'} = $lh->filename;
2118 my @filled_in = $self->print_generic( %params );
2120 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2124 ) or die "can't open temp file: $!\n";
2125 print $fh join('', @filled_in );
2128 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2129 return ($1, $params{'logo_file'});
2133 =item print_generic OPTION => VALUE ...
2135 Internal method - returns a filled-in template for this invoice as a scalar.
2137 See print_ps and print_pdf for methods that return PostScript and PDF output.
2139 Non optional options include
2140 format - latex, html, template
2142 Optional options include
2144 template - a value used as a suffix for a configuration template
2146 time - a value used to control the printing of overdue messages. The
2147 default is now. It isn't the date of the invoice; that's the `_date' field.
2148 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2149 L<Time::Local> and L<Date::Parse> for conversion functions.
2153 unsquelch_cdr - overrides any per customer cdr squelching when true
2155 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2159 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2160 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2161 # yes: fixed width (dot matrix) text printing will be borked
2164 my( $self, %params ) = @_;
2165 my $today = $params{today} ? $params{today} : time;
2166 warn "$me print_generic called on $self with suffix $params{template}\n"
2169 my $format = $params{format};
2170 die "Unknown format: $format"
2171 unless $format =~ /^(latex|html|template)$/;
2173 my $cust_main = $self->cust_main;
2174 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2175 unless $cust_main->payname
2176 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2178 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2179 'html' => [ '<%=', '%>' ],
2180 'template' => [ '{', '}' ],
2183 #create the template
2184 my $template = $params{template} ? $params{template} : $self->_agent_template;
2185 my $templatefile = "invoice_$format";
2186 $templatefile .= "_$template"
2187 if length($template);
2188 my @invoice_template = map "$_\n", $conf->config($templatefile)
2189 or die "cannot load config data $templatefile";
2192 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2193 #change this to a die when the old code is removed
2194 warn "old-style invoice template $templatefile; ".
2195 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2196 $old_latex = 'true';
2197 @invoice_template = _translate_old_latex_format(@invoice_template);
2200 my $text_template = new Text::Template(
2202 SOURCE => \@invoice_template,
2203 DELIMITERS => $delimiters{$format},
2206 $text_template->compile()
2207 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2210 # additional substitution could possibly cause breakage in existing templates
2211 my %convert_maps = (
2213 'notes' => sub { map "$_", @_ },
2214 'footer' => sub { map "$_", @_ },
2215 'smallfooter' => sub { map "$_", @_ },
2216 'returnaddress' => sub { map "$_", @_ },
2217 'coupon' => sub { map "$_", @_ },
2218 'summary' => sub { map "$_", @_ },
2224 s/%%(.*)$/<!-- $1 -->/g;
2225 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2226 s/\\begin\{enumerate\}/<ol>/g;
2228 s/\\end\{enumerate\}/<\/ol>/g;
2229 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2238 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2240 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2245 s/\\\\\*?\s*$/<BR>/;
2246 s/\\hyphenation\{[\w\s\-]+}//;
2251 'coupon' => sub { "" },
2252 'summary' => sub { "" },
2259 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2260 s/\\begin\{enumerate\}//g;
2262 s/\\end\{enumerate\}//g;
2263 s/\\textbf\{(.*)\}/$1/g;
2270 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2272 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2277 s/\\\\\*?\s*$/\n/; # dubious
2278 s/\\hyphenation\{[\w\s\-]+}//;
2282 'coupon' => sub { "" },
2283 'summary' => sub { "" },
2288 # hashes for differing output formats
2289 my %nbsps = ( 'latex' => '~',
2290 'html' => '', # '&nbps;' would be nice
2291 'template' => '', # not used
2293 my $nbsp = $nbsps{$format};
2295 my %escape_functions = ( 'latex' => \&_latex_escape,
2296 'html' => \&_html_escape_nbsp,#\&encode_entities,
2297 'template' => sub { shift },
2299 my $escape_function = $escape_functions{$format};
2300 my $escape_function_nonbsp = ($format eq 'html')
2301 ? \&_html_escape : $escape_function;
2303 my %date_formats = ( 'latex' => $date_format_long,
2304 'html' => $date_format_long,
2307 $date_formats{'html'} =~ s/ / /g;
2309 my $date_format = $date_formats{$format};
2311 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2313 'html' => sub { return '<b>'. shift(). '</b>'
2315 'template' => sub { shift },
2317 my $embolden_function = $embolden_functions{$format};
2320 # generate template variables
2323 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2327 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2333 $returnaddress = join("\n",
2334 $conf->config_orbase("invoice_${format}returnaddress", $template)
2337 } elsif ( grep /\S/,
2338 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2340 my $convert_map = $convert_maps{$format}{'returnaddress'};
2343 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2348 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2350 my $convert_map = $convert_maps{$format}{'returnaddress'};
2351 $returnaddress = join( "\n", &$convert_map(
2352 map { s/( {2,})/'~' x length($1)/eg;
2356 ( $conf->config('company_name', $self->cust_main->agentnum),
2357 $conf->config('company_address', $self->cust_main->agentnum),
2364 my $warning = "Couldn't find a return address; ".
2365 "do you need to set the company_address configuration value?";
2367 $returnaddress = $nbsp;
2368 #$returnaddress = $warning;
2372 my $agentnum = $self->cust_main->agentnum;
2374 my %invoice_data = (
2377 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2378 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2379 'returnaddress' => $returnaddress,
2380 'agent' => &$escape_function($cust_main->agent->agent),
2383 'invnum' => $self->invnum,
2384 'date' => time2str($date_format, $self->_date),
2385 'today' => time2str($date_format_long, $today),
2386 'terms' => $self->terms,
2387 'template' => $template, #params{'template'},
2388 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2389 'current_charges' => sprintf("%.2f", $self->charged),
2390 'duedate' => $self->due_date2str($rdate_format), #date_format?
2393 'custnum' => $cust_main->display_custnum,
2394 'agent_custid' => &$escape_function($cust_main->agent_custid),
2395 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2396 payname company address1 address2 city state zip fax
2400 'ship_enable' => $conf->exists('invoice-ship_address'),
2401 'unitprices' => $conf->exists('invoice-unitprice'),
2402 'smallernotes' => $conf->exists('invoice-smallernotes'),
2403 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2404 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2406 #layout info -- would be fancy to calc some of this and bury the template
2408 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2409 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2410 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2411 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2412 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2413 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2414 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2415 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2416 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2417 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2419 # better hang on to conf_dir for a while (for old templates)
2420 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2422 #these are only used when doing paged plaintext
2428 $invoice_data{finance_section} = '';
2429 if ( $conf->config('finance_pkgclass') ) {
2431 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2432 $invoice_data{finance_section} = $pkg_class->categoryname;
2434 $invoice_data{finance_amount} = '0.00';
2435 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2437 my $countrydefault = $conf->config('countrydefault') || 'US';
2438 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2439 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2440 my $method = $prefix.$_;
2441 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2443 $invoice_data{'ship_country'} = ''
2444 if ( $invoice_data{'ship_country'} eq $countrydefault );
2446 $invoice_data{'cid'} = $params{'cid'}
2449 if ( $cust_main->country eq $countrydefault ) {
2450 $invoice_data{'country'} = '';
2452 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2456 $invoice_data{'address'} = \@address;
2458 $cust_main->payname.
2459 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2460 ? " (P.O. #". $cust_main->payinfo. ")"
2464 push @address, $cust_main->company
2465 if $cust_main->company;
2466 push @address, $cust_main->address1;
2467 push @address, $cust_main->address2
2468 if $cust_main->address2;
2470 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2471 push @address, $invoice_data{'country'}
2472 if $invoice_data{'country'};
2474 while (scalar(@address) < 5);
2476 $invoice_data{'logo_file'} = $params{'logo_file'}
2477 if $params{'logo_file'};
2479 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2480 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2481 #my $balance_due = $self->owed + $pr_total - $cr_total;
2482 my $balance_due = $self->owed + $pr_total;
2483 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2484 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2485 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2486 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2488 my $summarypage = '';
2489 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2492 $invoice_data{'summarypage'} = $summarypage;
2494 #do variable substitution in notes, footer, smallfooter
2495 foreach my $include (qw( notes footer smallfooter coupon )) {
2497 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2500 if ( $conf->exists($inc_file, $agentnum)
2501 && length( $conf->config($inc_file, $agentnum) ) ) {
2503 @inc_src = $conf->config($inc_file, $agentnum);
2507 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2509 my $convert_map = $convert_maps{$format}{$include};
2511 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2512 s/--\@\]/$delimiters{$format}[1]/g;
2515 &$convert_map( $conf->config($inc_file, $agentnum) );
2519 my $inc_tt = new Text::Template (
2521 SOURCE => [ map "$_\n", @inc_src ],
2522 DELIMITERS => $delimiters{$format},
2523 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2525 unless ( $inc_tt->compile() ) {
2526 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2527 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2531 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2533 $invoice_data{$include} =~ s/\n+$//
2534 if ($format eq 'latex');
2537 $invoice_data{'po_line'} =
2538 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2539 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2542 my %money_chars = ( 'latex' => '',
2543 'html' => $conf->config('money_char') || '$',
2546 my $money_char = $money_chars{$format};
2548 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2549 'html' => $conf->config('money_char') || '$',
2552 my $other_money_char = $other_money_chars{$format};
2553 $invoice_data{'dollar'} = $other_money_char;
2555 my @detail_items = ();
2556 my @total_items = ();
2560 $invoice_data{'detail_items'} = \@detail_items;
2561 $invoice_data{'total_items'} = \@total_items;
2562 $invoice_data{'buf'} = \@buf;
2563 $invoice_data{'sections'} = \@sections;
2565 my $previous_section = { 'description' => 'Previous Charges',
2566 'subtotal' => $other_money_char.
2567 sprintf('%.2f', $pr_total),
2568 'summarized' => $summarypage ? 'Y' : '',
2570 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2571 join(' / ', map { $cust_main->balance_date_range(@$_) }
2572 $self->_prior_month30s
2574 if $conf->exists('invoice_include_aging');
2577 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2578 'subtotal' => $taxtotal, # adjusted below
2579 'summarized' => $summarypage ? 'Y' : '',
2581 my $tax_weight = _pkg_category($tax_section->{description})
2582 ? _pkg_category($tax_section->{description})->weight
2584 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2585 $tax_section->{'sort_weight'} = $tax_weight;
2588 my $adjusttotal = 0;
2589 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2590 'subtotal' => 0, # adjusted below
2591 'summarized' => $summarypage ? 'Y' : '',
2593 my $adjust_weight = _pkg_category($adjust_section->{description})
2594 ? _pkg_category($adjust_section->{description})->weight
2596 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2597 $adjust_section->{'sort_weight'} = $adjust_weight;
2599 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2600 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2601 $invoice_data{'multisection'} = $multisection;
2602 my $late_sections = [];
2603 my $extra_sections = [];
2604 my $extra_lines = ();
2605 if ( $multisection ) {
2606 ($extra_sections, $extra_lines) =
2607 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2608 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2610 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2612 push @detail_items, @$extra_lines if $extra_lines;
2614 $self->_items_sections( $late_sections, # this could stand a refactor
2616 $escape_function_nonbsp,
2620 if ($conf->exists('svc_phone_sections')) {
2621 my ($phone_sections, $phone_lines) =
2622 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2623 push @{$late_sections}, @$phone_sections;
2624 push @detail_items, @$phone_lines;
2627 push @sections, { 'description' => '', 'subtotal' => '' };
2630 unless ( $conf->exists('disable_previous_balance')
2631 || $conf->exists('previous_balance-summary_only')
2635 foreach my $line_item ( $self->_items_previous ) {
2638 ext_description => [],
2640 $detail->{'ref'} = $line_item->{'pkgnum'};
2641 $detail->{'quantity'} = 1;
2642 $detail->{'section'} = $previous_section;
2643 $detail->{'description'} = &$escape_function($line_item->{'description'});
2644 if ( exists $line_item->{'ext_description'} ) {
2645 @{$detail->{'ext_description'}} = map {
2646 &$escape_function($_);
2647 } @{$line_item->{'ext_description'}};
2649 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2650 $line_item->{'amount'};
2651 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2653 push @detail_items, $detail;
2654 push @buf, [ $detail->{'description'},
2655 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2661 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2662 push @buf, ['','-----------'];
2663 push @buf, [ 'Total Previous Balance',
2664 $money_char. sprintf("%10.2f", $pr_total) ];
2668 if ( $conf->exists('svc_phone-did-summary') ) {
2669 my ($didsummary,$minutes) = $self->_did_summary;
2670 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2672 { 'description' => $didsummary_desc,
2673 'ext_description' => [ $didsummary, $minutes ],
2678 foreach my $section (@sections, @$late_sections) {
2680 # begin some normalization
2681 $section->{'subtotal'} = $section->{'amount'}
2683 && !exists($section->{subtotal})
2684 && exists($section->{amount});
2686 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2687 if ( $invoice_data{finance_section} &&
2688 $section->{'description'} eq $invoice_data{finance_section} );
2690 $section->{'subtotal'} = $other_money_char.
2691 sprintf('%.2f', $section->{'subtotal'})
2694 # continue some normalization
2695 $section->{'amount'} = $section->{'subtotal'}
2699 if ( $section->{'description'} ) {
2700 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2705 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2707 $options{'section'} = $section if $multisection;
2708 $options{'format'} = $format;
2709 $options{'escape_function'} = $escape_function;
2710 $options{'format_function'} = sub { () } unless $unsquelched;
2711 $options{'unsquelched'} = $unsquelched;
2712 $options{'summary_page'} = $summarypage;
2713 $options{'skip_usage'} =
2714 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2715 $options{'multilocation'} = $multilocation;
2716 $options{'multisection'} = $multisection;
2718 foreach my $line_item ( $self->_items_pkg(%options) ) {
2720 ext_description => [],
2722 $detail->{'ref'} = $line_item->{'pkgnum'};
2723 $detail->{'quantity'} = $line_item->{'quantity'};
2724 $detail->{'section'} = $section;
2725 $detail->{'description'} = &$escape_function($line_item->{'description'});
2726 if ( exists $line_item->{'ext_description'} ) {
2727 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2729 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2730 $line_item->{'amount'};
2731 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2732 $line_item->{'unit_amount'};
2733 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2735 push @detail_items, $detail;
2736 push @buf, ( [ $detail->{'description'},
2737 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2739 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2743 if ( $section->{'description'} ) {
2744 push @buf, ( ['','-----------'],
2745 [ $section->{'description'}. ' sub-total',
2746 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2755 $invoice_data{current_less_finance} =
2756 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2758 if ( $multisection && !$conf->exists('disable_previous_balance')
2759 || $conf->exists('previous_balance-summary_only') )
2761 unshift @sections, $previous_section if $pr_total;
2764 foreach my $tax ( $self->_items_tax ) {
2766 $taxtotal += $tax->{'amount'};
2768 my $description = &$escape_function( $tax->{'description'} );
2769 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2771 if ( $multisection ) {
2773 my $money = $old_latex ? '' : $money_char;
2774 push @detail_items, {
2775 ext_description => [],
2778 description => $description,
2779 amount => $money. $amount,
2781 section => $tax_section,
2786 push @total_items, {
2787 'total_item' => $description,
2788 'total_amount' => $other_money_char. $amount,
2793 push @buf,[ $description,
2794 $money_char. $amount,
2801 $total->{'total_item'} = 'Sub-total';
2802 $total->{'total_amount'} =
2803 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2805 if ( $multisection ) {
2806 $tax_section->{'subtotal'} = $other_money_char.
2807 sprintf('%.2f', $taxtotal);
2808 $tax_section->{'pretotal'} = 'New charges sub-total '.
2809 $total->{'total_amount'};
2810 push @sections, $tax_section if $taxtotal;
2812 unshift @total_items, $total;
2815 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2817 push @buf,['','-----------'];
2818 push @buf,[( $conf->exists('disable_previous_balance')
2820 : 'Total New Charges'
2822 $money_char. sprintf("%10.2f",$self->charged) ];
2828 $item = $conf->config('previous_balance-exclude_from_total')
2829 || 'Total New Charges'
2830 if $conf->exists('previous_balance-exclude_from_total');
2831 my $amount = $self->charged +
2832 ( $conf->exists('disable_previous_balance') ||
2833 $conf->exists('previous_balance-exclude_from_total')
2837 $total->{'total_item'} = &$embolden_function($item);
2838 $total->{'total_amount'} =
2839 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2840 if ( $multisection ) {
2841 if ( $adjust_section->{'sort_weight'} ) {
2842 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2843 sprintf("%.2f", ($self->billing_balance || 0) );
2845 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2846 sprintf('%.2f', $self->charged );
2849 push @total_items, $total;
2851 push @buf,['','-----------'];
2854 sprintf( '%10.2f', $amount )
2859 unless ( $conf->exists('disable_previous_balance') ) {
2860 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2863 my $credittotal = 0;
2864 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2867 $total->{'total_item'} = &$escape_function($credit->{'description'});
2868 $credittotal += $credit->{'amount'};
2869 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2870 $adjusttotal += $credit->{'amount'};
2871 if ( $multisection ) {
2872 my $money = $old_latex ? '' : $money_char;
2873 push @detail_items, {
2874 ext_description => [],
2877 description => &$escape_function($credit->{'description'}),
2878 amount => $money. $credit->{'amount'},
2880 section => $adjust_section,
2883 push @total_items, $total;
2887 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2890 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2891 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2895 my $paymenttotal = 0;
2896 foreach my $payment ( $self->_items_payments ) {
2898 $total->{'total_item'} = &$escape_function($payment->{'description'});
2899 $paymenttotal += $payment->{'amount'};
2900 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2901 $adjusttotal += $payment->{'amount'};
2902 if ( $multisection ) {
2903 my $money = $old_latex ? '' : $money_char;
2904 push @detail_items, {
2905 ext_description => [],
2908 description => &$escape_function($payment->{'description'}),
2909 amount => $money. $payment->{'amount'},
2911 section => $adjust_section,
2914 push @total_items, $total;
2916 push @buf, [ $payment->{'description'},
2917 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2920 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2922 if ( $multisection ) {
2923 $adjust_section->{'subtotal'} = $other_money_char.
2924 sprintf('%.2f', $adjusttotal);
2925 push @sections, $adjust_section
2926 unless $adjust_section->{sort_weight};
2931 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2932 $total->{'total_amount'} =
2933 &$embolden_function(
2934 $other_money_char. sprintf('%.2f', $summarypage
2936 $self->billing_balance
2937 : $self->owed + $pr_total
2940 if ( $multisection && !$adjust_section->{sort_weight} ) {
2941 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2942 $total->{'total_amount'};
2944 push @total_items, $total;
2946 push @buf,['','-----------'];
2947 push @buf,[$self->balance_due_msg, $money_char.
2948 sprintf("%10.2f", $balance_due ) ];
2952 if ( $multisection ) {
2953 if ($conf->exists('svc_phone_sections')) {
2955 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2956 $total->{'total_amount'} =
2957 &$embolden_function(
2958 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2960 my $last_section = pop @sections;
2961 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2962 $total->{'total_amount'};
2963 push @sections, $last_section;
2965 push @sections, @$late_sections
2969 my @includelist = ();
2970 push @includelist, 'summary' if $summarypage;
2971 foreach my $include ( @includelist ) {
2973 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2976 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2978 @inc_src = $conf->config($inc_file, $agentnum);
2982 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2984 my $convert_map = $convert_maps{$format}{$include};
2986 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2987 s/--\@\]/$delimiters{$format}[1]/g;
2990 &$convert_map( $conf->config($inc_file, $agentnum) );
2994 my $inc_tt = new Text::Template (
2996 SOURCE => [ map "$_\n", @inc_src ],
2997 DELIMITERS => $delimiters{$format},
2998 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3000 unless ( $inc_tt->compile() ) {
3001 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3002 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3006 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3008 $invoice_data{$include} =~ s/\n+$//
3009 if ($format eq 'latex');
3014 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3015 /invoice_lines\((\d*)\)/;
3016 $invoice_lines += $1 || scalar(@buf);
3019 die "no invoice_lines() functions in template?"
3020 if ( $format eq 'template' && !$wasfunc );
3022 if ($format eq 'template') {
3024 if ( $invoice_lines ) {
3025 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3026 $invoice_data{'total_pages'}++
3027 if scalar(@buf) % $invoice_lines;
3030 #setup subroutine for the template
3031 sub FS::cust_bill::_template::invoice_lines {
3032 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3034 scalar(@FS::cust_bill::_template::buf)
3035 ? shift @FS::cust_bill::_template::buf
3044 push @collect, split("\n",
3045 $text_template->fill_in( HASH => \%invoice_data,
3046 PACKAGE => 'FS::cust_bill::_template'
3049 $FS::cust_bill::_template::page++;
3051 map "$_\n", @collect;
3053 warn "filling in template for invoice ". $self->invnum. "\n"
3055 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3058 $text_template->fill_in(HASH => \%invoice_data);
3062 # helper routine for generating date ranges
3063 sub _prior_month30s {
3066 [ 1, 2592000 ], # 0-30 days ago
3067 [ 2592000, 5184000 ], # 30-60 days ago
3068 [ 5184000, 7776000 ], # 60-90 days ago
3069 [ 7776000, 0 ], # 90+ days ago
3072 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3073 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3078 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3080 Returns an postscript invoice, as a scalar.
3082 Options can be passed as a hashref (recommended) or as a list of time, template
3083 and then any key/value pairs for any other options.
3085 I<time> an optional value used to control the printing of overdue messages. The
3086 default is now. It isn't the date of the invoice; that's the `_date' field.
3087 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3088 L<Time::Local> and L<Date::Parse> for conversion functions.
3090 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3097 my ($file, $lfile) = $self->print_latex(@_);
3098 my $ps = generate_ps($file);
3104 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3106 Returns an PDF invoice, as a scalar.
3108 Options can be passed as a hashref (recommended) or as a list of time, template
3109 and then any key/value pairs for any other options.
3111 I<time> an optional value used to control the printing of overdue messages. The
3112 default is now. It isn't the date of the invoice; that's the `_date' field.
3113 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3114 L<Time::Local> and L<Date::Parse> for conversion functions.
3116 I<template>, if specified, is the name of a suffix for alternate invoices.
3118 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3125 my ($file, $lfile) = $self->print_latex(@_);
3126 my $pdf = generate_pdf($file);
3132 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3134 Returns an HTML invoice, as a scalar.
3136 I<time> an optional value used to control the printing of overdue messages. The
3137 default is now. It isn't the date of the invoice; that's the `_date' field.
3138 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3139 L<Time::Local> and L<Date::Parse> for conversion functions.
3141 I<template>, if specified, is the name of a suffix for alternate invoices.
3143 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3145 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3146 when emailing the invoice as part of a multipart/related MIME email.
3154 %params = %{ shift() };
3156 $params{'time'} = shift;
3157 $params{'template'} = shift;
3158 $params{'cid'} = shift;
3161 $params{'format'} = 'html';
3163 $self->print_generic( %params );
3166 # quick subroutine for print_latex
3168 # There are ten characters that LaTeX treats as special characters, which
3169 # means that they do not simply typeset themselves:
3170 # # $ % & ~ _ ^ \ { }
3172 # TeX ignores blanks following an escaped character; if you want a blank (as
3173 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3177 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3178 $value =~ s/([<>])/\$$1\$/g;
3184 encode_entities($value);
3188 sub _html_escape_nbsp {
3189 my $value = _html_escape(shift);
3190 $value =~ s/ +/ /g;
3194 #utility methods for print_*
3196 sub _translate_old_latex_format {
3197 warn "_translate_old_latex_format called\n"
3204 if ( $line =~ /^%%Detail\s*$/ ) {
3206 push @template, q![@--!,
3207 q! foreach my $_tr_line (@detail_items) {!,
3208 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3209 q! $_tr_line->{'description'} .= !,
3210 q! "\\tabularnewline\n~~".!,
3211 q! join( "\\tabularnewline\n~~",!,
3212 q! @{$_tr_line->{'ext_description'}}!,
3216 while ( ( my $line_item_line = shift )
3217 !~ /^%%EndDetail\s*$/ ) {
3218 $line_item_line =~ s/'/\\'/g; # nice LTS
3219 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3220 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3221 push @template, " \$OUT .= '$line_item_line';";
3224 push @template, '}',
3227 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3229 push @template, '[@--',
3230 ' foreach my $_tr_line (@total_items) {';
3232 while ( ( my $total_item_line = shift )
3233 !~ /^%%EndTotalDetails\s*$/ ) {
3234 $total_item_line =~ s/'/\\'/g; # nice LTS
3235 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3236 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3237 push @template, " \$OUT .= '$total_item_line';";
3240 push @template, '}',
3244 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3245 push @template, $line;
3251 warn "$_\n" foreach @template;
3260 #check for an invoice-specific override
3261 return $self->invoice_terms if $self->invoice_terms;
3263 #check for a customer- specific override
3264 my $cust_main = $self->cust_main;
3265 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3267 #use configured default
3268 $conf->config('invoice_default_terms') || '';
3274 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3275 $duedate = $self->_date() + ( $1 * 86400 );
3282 $self->due_date ? time2str(shift, $self->due_date) : '';
3285 sub balance_due_msg {
3287 my $msg = 'Balance Due';
3288 return $msg unless $self->terms;
3289 if ( $self->due_date ) {
3290 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3291 } elsif ( $self->terms ) {
3292 $msg .= ' - '. $self->terms;
3297 sub balance_due_date {
3300 if ( $conf->exists('invoice_default_terms')
3301 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3302 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3307 =item invnum_date_pretty
3309 Returns a string with the invoice number and date, for example:
3310 "Invoice #54 (3/20/2008)"
3314 sub invnum_date_pretty {
3316 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3321 Returns a string with the date, for example: "3/20/2008"
3327 time2str($date_format, $self->_date);
3330 use vars qw(%pkg_category_cache);
3331 sub _items_sections {
3334 my $summarypage = shift;
3336 my $extra_sections = shift;
3340 my %late_subtotal = ();
3343 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3346 my $usage = $cust_bill_pkg->usage;
3348 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3349 next if ( $display->summary && $summarypage );
3351 my $section = $display->section;
3352 my $type = $display->type;
3354 $not_tax{$section} = 1
3355 unless $cust_bill_pkg->pkgnum == 0;
3357 if ( $display->post_total && !$summarypage ) {
3358 if (! $type || $type eq 'S') {
3359 $late_subtotal{$section} += $cust_bill_pkg->setup
3360 if $cust_bill_pkg->setup != 0;
3364 $late_subtotal{$section} += $cust_bill_pkg->recur
3365 if $cust_bill_pkg->recur != 0;
3368 if ($type && $type eq 'R') {
3369 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3370 if $cust_bill_pkg->recur != 0;
3373 if ($type && $type eq 'U') {
3374 $late_subtotal{$section} += $usage
3375 unless scalar(@$extra_sections);
3380 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3382 if (! $type || $type eq 'S') {
3383 $subtotal{$section} += $cust_bill_pkg->setup
3384 if $cust_bill_pkg->setup != 0;
3388 $subtotal{$section} += $cust_bill_pkg->recur
3389 if $cust_bill_pkg->recur != 0;
3392 if ($type && $type eq 'R') {
3393 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3394 if $cust_bill_pkg->recur != 0;
3397 if ($type && $type eq 'U') {
3398 $subtotal{$section} += $usage
3399 unless scalar(@$extra_sections);
3408 %pkg_category_cache = ();
3410 push @$late, map { { 'description' => &{$escape}($_),
3411 'subtotal' => $late_subtotal{$_},
3413 'sort_weight' => ( _pkg_category($_)
3414 ? _pkg_category($_)->weight
3417 ((_pkg_category($_) && _pkg_category($_)->condense)
3418 ? $self->_condense_section($format)
3422 sort _sectionsort keys %late_subtotal;
3425 if ( $summarypage ) {
3426 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3427 map { $_->categoryname } qsearch('pkg_category', {});
3428 push @sections, '' if exists($subtotal{''});
3430 @sections = keys %subtotal;
3433 my @early = map { { 'description' => &{$escape}($_),
3434 'subtotal' => $subtotal{$_},
3435 'summarized' => $not_tax{$_} ? '' : 'Y',
3436 'tax_section' => $not_tax{$_} ? '' : 'Y',
3437 'sort_weight' => ( _pkg_category($_)
3438 ? _pkg_category($_)->weight
3441 ((_pkg_category($_) && _pkg_category($_)->condense)
3442 ? $self->_condense_section($format)
3447 push @early, @$extra_sections if $extra_sections;
3449 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3453 #helper subs for above
3456 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3460 my $categoryname = shift;
3461 $pkg_category_cache{$categoryname} ||=
3462 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3465 my %condensed_format = (
3466 'label' => [ qw( Description Qty Amount ) ],
3468 sub { shift->{description} },
3469 sub { shift->{quantity} },
3470 sub { my($href, %opt) = @_;
3471 ($opt{dollar} || ''). $href->{amount};
3474 'align' => [ qw( l r r ) ],
3475 'span' => [ qw( 5 1 1 ) ], # unitprices?
3476 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3479 sub _condense_section {
3480 my ( $self, $format ) = ( shift, shift );
3482 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3483 qw( description_generator
3486 total_line_generator
3491 sub _condensed_generator_defaults {
3492 my ( $self, $format ) = ( shift, shift );
3493 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3502 sub _condensed_header_generator {
3503 my ( $self, $format ) = ( shift, shift );
3505 my ( $f, $prefix, $suffix, $separator, $column ) =
3506 _condensed_generator_defaults($format);
3508 if ($format eq 'latex') {
3509 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3510 $suffix = "\\\\\n\\hline";
3513 sub { my ($d,$a,$s,$w) = @_;
3514 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3516 } elsif ( $format eq 'html' ) {
3517 $prefix = '<th></th>';
3521 sub { my ($d,$a,$s,$w) = @_;
3522 return qq!<th align="$html_align{$a}">$d</th>!;
3530 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3532 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3535 $prefix. join($separator, @result). $suffix;
3540 sub _condensed_description_generator {
3541 my ( $self, $format ) = ( shift, shift );
3543 my ( $f, $prefix, $suffix, $separator, $column ) =
3544 _condensed_generator_defaults($format);
3546 my $money_char = '$';
3547 if ($format eq 'latex') {
3548 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3550 $separator = " & \n";
3552 sub { my ($d,$a,$s,$w) = @_;
3553 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3555 $money_char = '\\dollar';
3556 }elsif ( $format eq 'html' ) {
3557 $prefix = '"><td align="center"></td>';
3561 sub { my ($d,$a,$s,$w) = @_;
3562 return qq!<td align="$html_align{$a}">$d</td>!;
3564 #$money_char = $conf->config('money_char') || '$';
3565 $money_char = ''; # this is madness
3573 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3575 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3577 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3578 map { $f->{$_}->[$i] } qw(align span width)
3582 $prefix. join( $separator, @result ). $suffix;
3587 sub _condensed_total_generator {
3588 my ( $self, $format ) = ( shift, shift );
3590 my ( $f, $prefix, $suffix, $separator, $column ) =
3591 _condensed_generator_defaults($format);
3594 if ($format eq 'latex') {
3597 $separator = " & \n";
3599 sub { my ($d,$a,$s,$w) = @_;
3600 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3602 }elsif ( $format eq 'html' ) {
3606 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3608 sub { my ($d,$a,$s,$w) = @_;
3609 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3618 # my $r = &{$f->{fields}->[$i]}(@args);
3619 # $r .= ' Total' unless $i;
3621 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3623 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3624 map { $f->{$_}->[$i] } qw(align span width)
3628 $prefix. join( $separator, @result ). $suffix;
3633 =item total_line_generator FORMAT
3635 Returns a coderef used for generation of invoice total line items for this
3636 usage_class. FORMAT is either html or latex
3640 # should not be used: will have issues with hash element names (description vs
3641 # total_item and amount vs total_amount -- another array of functions?
3643 sub _condensed_total_line_generator {
3644 my ( $self, $format ) = ( shift, shift );
3646 my ( $f, $prefix, $suffix, $separator, $column ) =
3647 _condensed_generator_defaults($format);
3650 if ($format eq 'latex') {
3653 $separator = " & \n";
3655 sub { my ($d,$a,$s,$w) = @_;
3656 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3658 }elsif ( $format eq 'html' ) {
3662 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3664 sub { my ($d,$a,$s,$w) = @_;
3665 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3674 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3676 &{$column}( &{$f->{fields}->[$i]}(@args),
3677 map { $f->{$_}->[$i] } qw(align span width)
3681 $prefix. join( $separator, @result ). $suffix;
3686 #sub _items_extra_usage_sections {
3688 # my $escape = shift;
3690 # my %sections = ();
3692 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3693 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3695 # next unless $cust_bill_pkg->pkgnum > 0;
3697 # foreach my $section ( keys %usage_class ) {
3699 # my $usage = $cust_bill_pkg->usage($section);
3701 # next unless $usage && $usage > 0;
3703 # $sections{$section} ||= 0;
3704 # $sections{$section} += $usage;
3710 # map { { 'description' => &{$escape}($_),
3711 # 'subtotal' => $sections{$_},
3712 # 'summarized' => '',
3713 # 'tax_section' => '',
3716 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3720 sub _items_extra_usage_sections {
3729 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3730 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3731 next unless $cust_bill_pkg->pkgnum > 0;
3733 foreach my $classnum ( keys %usage_class ) {
3734 my $section = $usage_class{$classnum}->classname;
3735 $classnums{$section} = $classnum;
3737 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3738 my $amount = $detail->amount;
3739 next unless $amount && $amount > 0;
3741 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3742 $sections{$section}{amount} += $amount; #subtotal
3743 $sections{$section}{calls}++;
3744 $sections{$section}{duration} += $detail->duration;
3746 my $desc = $detail->regionname;
3747 my $description = $desc;
3748 $description = substr($desc, 0, 50). '...'
3749 if $format eq 'latex' && length($desc) > 50;
3751 $lines{$section}{$desc} ||= {
3752 description => &{$escape}($description),
3753 #pkgpart => $part_pkg->pkgpart,
3754 pkgnum => $cust_bill_pkg->pkgnum,
3759 #unit_amount => $cust_bill_pkg->unitrecur,
3760 quantity => $cust_bill_pkg->quantity,
3761 product_code => 'N/A',
3762 ext_description => [],
3765 $lines{$section}{$desc}{amount} += $amount;
3766 $lines{$section}{$desc}{calls}++;
3767 $lines{$section}{$desc}{duration} += $detail->duration;
3773 my %sectionmap = ();
3774 foreach (keys %sections) {
3775 my $usage_class = $usage_class{$classnums{$_}};
3776 $sectionmap{$_} = { 'description' => &{$escape}($_),
3777 'amount' => $sections{$_}{amount}, #subtotal
3778 'calls' => $sections{$_}{calls},
3779 'duration' => $sections{$_}{duration},
3781 'tax_section' => '',
3782 'sort_weight' => $usage_class->weight,
3783 ( $usage_class->format
3784 ? ( map { $_ => $usage_class->$_($format) }
3785 qw( description_generator header_generator total_generator total_line_generator )
3792 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3796 foreach my $section ( keys %lines ) {
3797 foreach my $line ( keys %{$lines{$section}} ) {
3798 my $l = $lines{$section}{$line};
3799 $l->{section} = $sectionmap{$section};
3800 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3801 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3806 return(\@sections, \@lines);
3812 my $end = $self->_date;
3813 my $start = $end - 2592000; # 30 days
3814 my $cust_main = $self->cust_main;
3815 my @pkgs = $cust_main->all_pkgs;
3816 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3819 foreach my $pkg ( @pkgs ) {
3820 my @h_cust_svc = $pkg->h_cust_svc($end);
3821 foreach my $h_cust_svc ( @h_cust_svc ) {
3822 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3823 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3825 my $inserted = $h_cust_svc->date_inserted;
3826 my $deleted = $h_cust_svc->date_deleted;
3827 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3829 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3831 # DID either activated or ported in; cannot be both for same DID simultaneously
3832 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3833 && (!$phone_inserted->lnp_status
3834 || $phone_inserted->lnp_status eq ''
3835 || $phone_inserted->lnp_status eq 'native')) {
3838 else { # this one not so clean, should probably move to (h_)svc_phone
3839 my $phone_portedin = qsearchs( 'h_svc_phone',
3840 { 'svcnum' => $h_cust_svc->svcnum,
3841 'lnp_status' => 'portedin' },
3842 FS::h_svc_phone->sql_h_searchs($end),
3844 $num_portedin++ if $phone_portedin;
3847 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3848 if($deleted >= $start && $deleted <= $end && $phone_deleted
3849 && (!$phone_deleted->lnp_status
3850 || $phone_deleted->lnp_status ne 'portingout')) {
3853 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3854 && $phone_deleted->lnp_status
3855 && $phone_deleted->lnp_status eq 'portingout') {
3859 # increment usage minutes
3860 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3861 foreach my $cdr ( @cdrs ) {
3862 $minutes += $cdr->billsec/60;
3865 # don't look at this service again
3866 push @seen, $h_cust_svc->svcnum;
3870 $minutes = sprintf("%d", $minutes);
3871 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3872 . "$num_deactivated Ported-Out: $num_portedout ",
3873 "Total Minutes: $minutes");
3876 sub _items_svc_phone_sections {
3885 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3886 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3888 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3889 next unless $cust_bill_pkg->pkgnum > 0;
3891 my @header = $cust_bill_pkg->details_header;
3892 next unless scalar(@header);
3894 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3896 my $phonenum = $detail->phonenum;
3897 next unless $phonenum;
3899 my $amount = $detail->amount;
3900 next unless $amount && $amount > 0;
3902 $sections{$phonenum} ||= { 'amount' => 0,
3905 'sort_weight' => -1,
3906 'phonenum' => $phonenum,
3908 $sections{$phonenum}{amount} += $amount; #subtotal
3909 $sections{$phonenum}{calls}++;
3910 $sections{$phonenum}{duration} += $detail->duration;
3912 my $desc = $detail->regionname;
3913 my $description = $desc;
3914 $description = substr($desc, 0, 50). '...'
3915 if $format eq 'latex' && length($desc) > 50;
3917 $lines{$phonenum}{$desc} ||= {
3918 description => &{$escape}($description),
3919 #pkgpart => $part_pkg->pkgpart,
3927 product_code => 'N/A',
3928 ext_description => [],
3931 $lines{$phonenum}{$desc}{amount} += $amount;
3932 $lines{$phonenum}{$desc}{calls}++;
3933 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3935 my $line = $usage_class{$detail->classnum}->classname;
3936 $sections{"$phonenum $line"} ||=
3940 'sort_weight' => $usage_class{$detail->classnum}->weight,
3941 'phonenum' => $phonenum,
3942 'header' => [ @header ],
3944 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3945 $sections{"$phonenum $line"}{calls}++;
3946 $sections{"$phonenum $line"}{duration} += $detail->duration;
3948 $lines{"$phonenum $line"}{$desc} ||= {
3949 description => &{$escape}($description),
3950 #pkgpart => $part_pkg->pkgpart,
3958 product_code => 'N/A',
3959 ext_description => [],
3962 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3963 $lines{"$phonenum $line"}{$desc}{calls}++;
3964 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3965 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3966 $detail->formatted('format' => $format);
3971 my %sectionmap = ();
3972 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3973 foreach ( keys %sections ) {
3974 my @header = @{ $sections{$_}{header} || [] };
3976 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
3977 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3978 my $usage_class = $summary ? $simple : $usage_simple;
3979 my $ending = $summary ? ' usage charges' : '';
3982 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
3984 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3985 'amount' => $sections{$_}{amount}, #subtotal
3986 'calls' => $sections{$_}{calls},
3987 'duration' => $sections{$_}{duration},
3989 'tax_section' => '',
3990 'phonenum' => $sections{$_}{phonenum},
3991 'sort_weight' => $sections{$_}{sort_weight},
3992 'post_total' => $summary, #inspire pagebreak
3994 ( map { $_ => $usage_class->$_($format, %gen_opt) }
3995 qw( description_generator
3998 total_line_generator
4005 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4006 $a->{sort_weight} <=> $b->{sort_weight}
4011 foreach my $section ( keys %lines ) {
4012 foreach my $line ( keys %{$lines{$section}} ) {
4013 my $l = $lines{$section}{$line};
4014 $l->{section} = $sectionmap{$section};
4015 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4016 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4021 return(\@sections, \@lines);
4028 #my @display = scalar(@_)
4030 # : qw( _items_previous _items_pkg );
4031 # #: qw( _items_pkg );
4032 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4033 my @display = qw( _items_previous _items_pkg );
4036 foreach my $display ( @display ) {
4037 push @b, $self->$display(@_);
4042 sub _items_previous {
4044 my $cust_main = $self->cust_main;
4045 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4047 foreach ( @pr_cust_bill ) {
4048 my $date = $conf->exists('invoice_show_prior_due_date')
4049 ? 'due '. $_->due_date2str($date_format)
4050 : time2str($date_format, $_->_date);
4052 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4053 #'pkgpart' => 'N/A',
4055 'amount' => sprintf("%.2f", $_->owed),
4061 # 'description' => 'Previous Balance',
4062 # #'pkgpart' => 'N/A',
4063 # 'pkgnum' => 'N/A',
4064 # 'amount' => sprintf("%10.2f", $pr_total ),
4065 # 'ext_description' => [ map {
4066 # "Invoice ". $_->invnum.
4067 # " (". time2str("%x",$_->_date). ") ".
4068 # sprintf("%10.2f", $_->owed)
4069 # } @pr_cust_bill ],
4077 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4078 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4079 if ($options{section} && $options{section}->{condensed}) {
4081 local $Storable::canonical = 1;
4082 foreach ( @items ) {
4084 delete $item->{ref};
4085 delete $item->{ext_description};
4086 my $key = freeze($item);
4087 $itemshash{$key} ||= 0;
4088 $itemshash{$key} ++; # += $item->{quantity};
4090 @items = sort { $a->{description} cmp $b->{description} }
4091 map { my $i = thaw($_);
4092 $i->{quantity} = $itemshash{$_};
4094 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4103 return 0 unless $a->itemdesc cmp $b->itemdesc;
4104 return -1 if $b->itemdesc eq 'Tax';
4105 return 1 if $a->itemdesc eq 'Tax';
4106 return -1 if $b->itemdesc eq 'Other surcharges';
4107 return 1 if $a->itemdesc eq 'Other surcharges';
4108 $a->itemdesc cmp $b->itemdesc;
4113 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4114 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4117 sub _items_cust_bill_pkg {
4119 my $cust_bill_pkg = shift;
4122 my $format = $opt{format} || '';
4123 my $escape_function = $opt{escape_function} || sub { shift };
4124 my $format_function = $opt{format_function} || '';
4125 my $unsquelched = $opt{unsquelched} || '';
4126 my $section = $opt{section}->{description} if $opt{section};
4127 my $summary_page = $opt{summary_page} || '';
4128 my $multilocation = $opt{multilocation} || '';
4129 my $multisection = $opt{multisection} || '';
4130 my $discount_show_always = 0;
4133 my ($s, $r, $u) = ( undef, undef, undef );
4134 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4137 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4138 && $conf->exists('discount-show-always'));
4140 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4141 if ( $_ && !$cust_bill_pkg->hidden ) {
4142 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4143 $_->{amount} =~ s/^\-0\.00$/0.00/;
4144 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4146 unless ( $_->{amount} == 0 && !$discount_show_always );
4151 foreach my $display ( grep { defined($section)
4152 ? $_->section eq $section
4155 #grep { !$_->summary || !$summary_page } # bunk!
4156 grep { !$_->summary || $multisection }
4157 $cust_bill_pkg->cust_bill_pkg_display
4161 my $type = $display->type;
4163 my $desc = $cust_bill_pkg->desc;
4164 $desc = substr($desc, 0, 50). '...'
4165 if $format eq 'latex' && length($desc) > 50;
4167 my %details_opt = ( 'format' => $format,
4168 'escape_function' => $escape_function,
4169 'format_function' => $format_function,
4172 if ( $cust_bill_pkg->pkgnum > 0 ) {
4174 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4176 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4178 my $description = $desc;
4179 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4182 unless ( $cust_pkg->part_pkg->hide_svc_detail
4183 || $cust_bill_pkg->hidden )
4186 push @d, map &{$escape_function}($_),
4187 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4188 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4190 if ( $multilocation ) {
4191 my $loc = $cust_pkg->location_label;
4192 $loc = substr($loc, 0, 50). '...'
4193 if $format eq 'latex' && length($loc) > 50;
4194 push @d, &{$escape_function}($loc);
4199 push @d, $cust_bill_pkg->details(%details_opt)
4200 if $cust_bill_pkg->recur == 0;
4202 if ( $cust_bill_pkg->hidden ) {
4203 $s->{amount} += $cust_bill_pkg->setup;
4204 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4205 push @{ $s->{ext_description} }, @d;
4208 description => $description,
4209 #pkgpart => $part_pkg->pkgpart,
4210 pkgnum => $cust_bill_pkg->pkgnum,
4211 amount => $cust_bill_pkg->setup,
4212 unit_amount => $cust_bill_pkg->unitsetup,
4213 quantity => $cust_bill_pkg->quantity,
4214 ext_description => \@d,
4220 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4221 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4222 ( !$type || $type eq 'R' || $type eq 'U' )
4226 my $is_summary = $display->summary;
4227 my $description = ($is_summary && $type && $type eq 'U')
4228 ? "Usage charges" : $desc;
4230 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4231 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4232 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4237 #at least until cust_bill_pkg has "past" ranges in addition to
4238 #the "future" sdate/edate ones... see #3032
4239 my @dates = ( $self->_date );
4240 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4241 push @dates, $prev->sdate if $prev;
4242 push @dates, undef if !$prev;
4244 unless ( $cust_pkg->part_pkg->hide_svc_detail
4245 || $cust_bill_pkg->itemdesc
4246 || $cust_bill_pkg->hidden
4247 || $is_summary && $type && $type eq 'U' )
4250 push @d, map &{$escape_function}($_),
4251 $cust_pkg->h_labels_short(@dates, 'I')
4252 #$cust_bill_pkg->edate,
4253 #$cust_bill_pkg->sdate)
4254 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4256 if ( $multilocation ) {
4257 my $loc = $cust_pkg->location_label;
4258 $loc = substr($loc, 0, 50). '...'
4259 if $format eq 'latex' && length($loc) > 50;
4260 push @d, &{$escape_function}($loc);
4265 push @d, $cust_bill_pkg->details(%details_opt)
4266 unless ($is_summary || $type && $type eq 'R');
4270 $amount = $cust_bill_pkg->recur;
4271 }elsif($type eq 'R') {
4272 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4273 }elsif($type eq 'U') {
4274 $amount = $cust_bill_pkg->usage;
4277 if ( !$type || $type eq 'R' ) {
4279 if ( $cust_bill_pkg->hidden ) {
4280 $r->{amount} += $amount;
4281 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4282 push @{ $r->{ext_description} }, @d;
4285 description => $description,
4286 #pkgpart => $part_pkg->pkgpart,
4287 pkgnum => $cust_bill_pkg->pkgnum,
4289 unit_amount => $cust_bill_pkg->unitrecur,
4290 quantity => $cust_bill_pkg->quantity,
4291 ext_description => \@d,
4295 } else { # $type eq 'U'
4297 if ( $cust_bill_pkg->hidden ) {
4298 $u->{amount} += $amount;
4299 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4300 push @{ $u->{ext_description} }, @d;
4303 description => $description,
4304 #pkgpart => $part_pkg->pkgpart,
4305 pkgnum => $cust_bill_pkg->pkgnum,
4307 unit_amount => $cust_bill_pkg->unitrecur,
4308 quantity => $cust_bill_pkg->quantity,
4309 ext_description => \@d,
4315 } # recurring or usage with recurring charge
4317 } else { #pkgnum tax or one-shot line item (??)
4319 if ( $cust_bill_pkg->setup != 0 ) {
4321 'description' => $desc,
4322 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4325 if ( $cust_bill_pkg->recur != 0 ) {
4327 'description' => "$desc (".
4328 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4329 time2str($date_format, $cust_bill_pkg->edate). ')',
4330 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4340 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4342 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4343 $_->{amount} =~ s/^\-0\.00$/0.00/;
4344 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4346 unless ( $_->{amount} == 0 && !$discount_show_always );
4354 sub _items_credits {
4355 my( $self, %opt ) = @_;
4356 my $trim_len = $opt{'trim_len'} || 60;
4360 foreach ( $self->cust_credited ) {
4362 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4364 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4365 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4366 $reason = " ($reason) " if $reason;
4369 #'description' => 'Credit ref\#'. $_->crednum.
4370 # " (". time2str("%x",$_->cust_credit->_date) .")".
4372 'description' => 'Credit applied '.
4373 time2str($date_format,$_->cust_credit->_date). $reason,
4374 'amount' => sprintf("%.2f",$_->amount),
4382 sub _items_payments {
4386 #get & print payments
4387 foreach ( $self->cust_bill_pay ) {
4389 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4392 'description' => "Payment received ".
4393 time2str($date_format,$_->cust_pay->_date ),
4394 'amount' => sprintf("%.2f", $_->amount )
4402 =item call_details [ OPTION => VALUE ... ]
4404 Returns an array of CSV strings representing the call details for this invoice
4405 The only option available is the boolean prepend_billed_number
4410 my ($self, %opt) = @_;
4412 my $format_function = sub { shift };
4414 if ($opt{prepend_billed_number}) {
4415 $format_function = sub {
4419 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4424 my @details = map { $_->details( 'format_function' => $format_function,
4425 'escape_function' => sub{ return() },
4429 $self->cust_bill_pkg;
4430 my $header = $details[0];
4431 ( $header, grep { $_ ne $header } @details );
4441 =item process_reprint
4445 sub process_reprint {
4446 process_re_X('print', @_);
4449 =item process_reemail
4453 sub process_reemail {
4454 process_re_X('email', @_);
4462 process_re_X('fax', @_);
4470 process_re_X('ftp', @_);
4477 sub process_respool {
4478 process_re_X('spool', @_);
4481 use Storable qw(thaw);
4485 my( $method, $job ) = ( shift, shift );
4486 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4488 my $param = thaw(decode_base64(shift));
4489 warn Dumper($param) if $DEBUG;
4500 my($method, $job, %param ) = @_;
4502 warn "re_X $method for job $job with param:\n".
4503 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4506 #some false laziness w/search/cust_bill.html
4508 my $orderby = 'ORDER BY cust_bill._date';
4510 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4512 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4514 my @cust_bill = qsearch( {
4515 #'select' => "cust_bill.*",
4516 'table' => 'cust_bill',
4517 'addl_from' => $addl_from,
4519 'extra_sql' => $extra_sql,
4520 'order_by' => $orderby,
4524 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4526 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4529 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4530 foreach my $cust_bill ( @cust_bill ) {
4531 $cust_bill->$method();
4533 if ( $job ) { #progressbar foo
4535 if ( time - $min_sec > $last ) {
4536 my $error = $job->update_statustext(
4537 int( 100 * $num / scalar(@cust_bill) )
4539 die $error if $error;
4550 =head1 CLASS METHODS
4556 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4561 my ($class, $start, $end) = @_;
4563 $class->paid_sql($start, $end). ' - '.
4564 $class->credited_sql($start, $end);
4569 Returns an SQL fragment to retreive the net amount (charged minus credited).
4574 my ($class, $start, $end) = @_;
4575 'charged - '. $class->credited_sql($start, $end);
4580 Returns an SQL fragment to retreive the amount paid against this invoice.
4585 my ($class, $start, $end) = @_;
4586 $start &&= "AND cust_bill_pay._date <= $start";
4587 $end &&= "AND cust_bill_pay._date > $end";
4588 $start = '' unless defined($start);
4589 $end = '' unless defined($end);
4590 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4591 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4596 Returns an SQL fragment to retreive the amount credited against this invoice.
4601 my ($class, $start, $end) = @_;
4602 $start &&= "AND cust_credit_bill._date <= $start";
4603 $end &&= "AND cust_credit_bill._date > $end";
4604 $start = '' unless defined($start);
4605 $end = '' unless defined($end);
4606 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4607 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4612 Returns an SQL fragment to retrieve the due date of an invoice.
4613 Currently only supported on PostgreSQL.
4621 cust_bill.invoice_terms,
4622 cust_main.invoice_terms,
4623 \''.($conf->config('invoice_default_terms') || '').'\'
4624 ), E\'Net (\\\\d+)\'
4626 ) * 86400 + cust_bill._date'
4629 =item search_sql_where HASHREF
4631 Class method which returns an SQL WHERE fragment to search for parameters
4632 specified in HASHREF. Valid parameters are
4638 List reference of start date, end date, as UNIX timestamps.
4648 List reference of charged limits (exclusive).
4652 List reference of charged limits (exclusive).
4656 flag, return open invoices only
4660 flag, return net invoices only
4664 =item newest_percust
4668 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4672 sub search_sql_where {
4673 my($class, $param) = @_;
4675 warn "$me search_sql_where called with params: \n".
4676 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4682 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4683 push @search, "cust_main.agentnum = $1";
4687 if ( $param->{_date} ) {
4688 my($beginning, $ending) = @{$param->{_date}};
4690 push @search, "cust_bill._date >= $beginning",
4691 "cust_bill._date < $ending";
4695 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4696 push @search, "cust_bill.invnum >= $1";
4698 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4699 push @search, "cust_bill.invnum <= $1";
4703 if ( $param->{charged} ) {
4704 my @charged = ref($param->{charged})
4705 ? @{ $param->{charged} }
4706 : ($param->{charged});
4708 push @search, map { s/^charged/cust_bill.charged/; $_; }
4712 my $owed_sql = FS::cust_bill->owed_sql;
4715 if ( $param->{owed} ) {
4716 my @owed = ref($param->{owed})
4717 ? @{ $param->{owed} }
4719 push @search, map { s/^owed/$owed_sql/; $_; }
4724 push @search, "0 != $owed_sql"
4725 if $param->{'open'};
4726 push @search, '0 != '. FS::cust_bill->net_sql
4730 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4731 if $param->{'days'};
4734 if ( $param->{'newest_percust'} ) {
4736 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4737 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4739 my @newest_where = map { my $x = $_;
4740 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4743 grep ! /^cust_main./, @search;
4744 my $newest_where = scalar(@newest_where)
4745 ? ' AND '. join(' AND ', @newest_where)
4749 push @search, "cust_bill._date = (
4750 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4751 WHERE newest_cust_bill.custnum = cust_bill.custnum
4757 #agent virtualization
4758 my $curuser = $FS::CurrentUser::CurrentUser;
4759 if ( $curuser->username eq 'fs_queue'
4760 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4762 my $newuser = qsearchs('access_user', {
4763 'username' => $username,
4767 $curuser = $newuser;
4769 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4772 push @search, $curuser->agentnums_sql;
4774 join(' AND ', @search );
4786 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4787 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base