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 if ( ! @invoicing_list ) { #no recipients
1245 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1246 die 'No recipients for customer #'. $self->custnum;
1248 #default: better to notify this person than silence
1249 @invoicing_list = ($invoice_from);
1253 my $subject = $self->email_subject($template);
1255 my $error = send_email(
1256 $self->generate_email(
1257 'from' => $invoice_from,
1258 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1259 'subject' => $subject,
1260 'template' => $template,
1261 'notice_name' => $notice_name,
1264 die "can't email invoice: $error\n" if $error;
1265 #die "$error\n" if $error;
1272 #my $template = scalar(@_) ? shift : '';
1275 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1278 my $cust_main = $self->cust_main;
1279 my $name = $cust_main->name;
1280 my $name_short = $cust_main->name_short;
1281 my $invoice_number = $self->invnum;
1282 my $invoice_date = $self->_date_pretty;
1284 eval qq("$subject");
1287 =item lpr_data HASHREF | [ TEMPLATE ]
1289 Returns the postscript or plaintext for this invoice as an arrayref.
1291 Options can be passed as a hashref (recommended) or as a single optional value
1294 I<template>, if specified, is the name of a suffix for alternate invoices.
1296 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1302 my( $template, $notice_name );
1305 $template = $opt->{'template'} || '';
1306 $notice_name = $opt->{'notice_name'} || 'Invoice';
1308 $template = scalar(@_) ? shift : '';
1309 $notice_name = 'Invoice';
1313 'template' => $template,
1314 'notice_name' => $notice_name,
1317 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1318 [ $self->$method( \%opt ) ];
1321 =item print HASHREF | [ TEMPLATE ]
1323 Prints this invoice.
1325 Options can be passed as a hashref (recommended) or as a single optional
1328 I<template>, if specified, is the name of a suffix for alternate invoices.
1330 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1334 #sub print_invoice {
1337 my( $template, $notice_name );
1340 $template = $opt->{'template'} || '';
1341 $notice_name = $opt->{'notice_name'} || 'Invoice';
1343 $template = scalar(@_) ? shift : '';
1344 $notice_name = 'Invoice';
1348 'template' => $template,
1349 'notice_name' => $notice_name,
1352 if($conf->exists('invoice_print_pdf')) {
1353 # Add the invoice to the current batch.
1354 $self->batch_invoice(\%opt);
1357 do_print $self->lpr_data(\%opt);
1361 =item fax_invoice HASHREF | [ TEMPLATE ]
1365 Options can be passed as a hashref (recommended) or as a single optional
1368 I<template>, if specified, is the name of a suffix for alternate invoices.
1370 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1376 my( $template, $notice_name );
1379 $template = $opt->{'template'} || '';
1380 $notice_name = $opt->{'notice_name'} || 'Invoice';
1382 $template = scalar(@_) ? shift : '';
1383 $notice_name = 'Invoice';
1386 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1387 unless $conf->exists('invoice_latex');
1389 my $dialstring = $self->cust_main->getfield('fax');
1393 'template' => $template,
1394 'notice_name' => $notice_name,
1397 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1398 'dialstring' => $dialstring,
1400 die $error if $error;
1404 =item batch_invoice [ HASHREF ]
1406 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1407 isn't an open batch, one will be created.
1412 my ($self, $opt) = @_;
1413 my $batch = FS::bill_batch->get_open_batch;
1414 my $cust_bill_batch = FS::cust_bill_batch->new({
1415 batchnum => $batch->batchnum,
1416 invnum => $self->invnum,
1418 return $cust_bill_batch->insert($opt);
1421 =item ftp_invoice [ TEMPLATENAME ]
1423 Sends this invoice data via FTP.
1425 TEMPLATENAME is unused?
1431 my $template = scalar(@_) ? shift : '';
1434 'protocol' => 'ftp',
1435 'server' => $conf->config('cust_bill-ftpserver'),
1436 'username' => $conf->config('cust_bill-ftpusername'),
1437 'password' => $conf->config('cust_bill-ftppassword'),
1438 'dir' => $conf->config('cust_bill-ftpdir'),
1439 'format' => $conf->config('cust_bill-ftpformat'),
1443 =item spool_invoice [ TEMPLATENAME ]
1445 Spools this invoice data (see L<FS::spool_csv>)
1447 TEMPLATENAME is unused?
1453 my $template = scalar(@_) ? shift : '';
1456 'format' => $conf->config('cust_bill-spoolformat'),
1457 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1461 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1463 Like B<send>, but only sends the invoice if it is the newest open invoice for
1468 sub send_if_newest {
1473 grep { $_->owed > 0 }
1474 qsearch('cust_bill', {
1475 'custnum' => $self->custnum,
1476 #'_date' => { op=>'>', value=>$self->_date },
1477 'invnum' => { op=>'>', value=>$self->invnum },
1484 =item send_csv OPTION => VALUE, ...
1486 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1490 protocol - currently only "ftp"
1496 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1497 and YYMMDDHHMMSS is a timestamp.
1499 See L</print_csv> for a description of the output format.
1504 my($self, %opt) = @_;
1508 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1509 mkdir $spooldir, 0700 unless -d $spooldir;
1511 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1512 my $file = "$spooldir/$tracctnum.csv";
1514 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1516 open(CSV, ">$file") or die "can't open $file: $!";
1524 if ( $opt{protocol} eq 'ftp' ) {
1525 eval "use Net::FTP;";
1527 $net = Net::FTP->new($opt{server}) or die @$;
1529 die "unknown protocol: $opt{protocol}";
1532 $net->login( $opt{username}, $opt{password} )
1533 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1535 $net->binary or die "can't set binary mode";
1537 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1539 $net->put($file) or die "can't put $file: $!";
1549 Spools CSV invoice data.
1555 =item format - 'default' or 'billco'
1557 =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>).
1559 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1561 =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.
1568 my($self, %opt) = @_;
1570 my $cust_main = $self->cust_main;
1572 if ( $opt{'dest'} ) {
1573 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1574 $cust_main->invoicing_list;
1575 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1576 || ! keys %invoicing_list;
1579 if ( $opt{'balanceover'} ) {
1581 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1584 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1585 mkdir $spooldir, 0700 unless -d $spooldir;
1587 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1591 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1592 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1595 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1597 open(CSV, ">>$file") or die "can't open $file: $!";
1598 flock(CSV, LOCK_EX);
1603 if ( lc($opt{'format'}) eq 'billco' ) {
1605 flock(CSV, LOCK_UN);
1610 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1613 open(CSV,">>$file") or die "can't open $file: $!";
1614 flock(CSV, LOCK_EX);
1620 flock(CSV, LOCK_UN);
1627 =item print_csv OPTION => VALUE, ...
1629 Returns CSV data for this invoice.
1633 format - 'default' or 'billco'
1635 Returns a list consisting of two scalars. The first is a single line of CSV
1636 header information for this invoice. The second is one or more lines of CSV
1637 detail information for this invoice.
1639 If I<format> is not specified or "default", the fields of the CSV file are as
1642 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1646 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1648 B<record_type> is C<cust_bill> for the initial header line only. The
1649 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1650 fields are filled in.
1652 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1653 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1656 =item invnum - invoice number
1658 =item custnum - customer number
1660 =item _date - invoice date
1662 =item charged - total invoice amount
1664 =item first - customer first name
1666 =item last - customer first name
1668 =item company - company name
1670 =item address1 - address line 1
1672 =item address2 - address line 1
1682 =item pkg - line item description
1684 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1686 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1688 =item sdate - start date for recurring fee
1690 =item edate - end date for recurring fee
1694 If I<format> is "billco", the fields of the header CSV file are as follows:
1696 +-------------------------------------------------------------------+
1697 | FORMAT HEADER FILE |
1698 |-------------------------------------------------------------------|
1699 | Field | Description | Name | Type | Width |
1700 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1701 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1702 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1703 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1704 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1705 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1706 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1707 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1708 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1709 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1710 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1711 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1712 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1713 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1714 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1715 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1716 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1717 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1718 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1719 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1720 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1721 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1722 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1723 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1724 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1725 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1726 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1727 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1728 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1729 +-------+-------------------------------+------------+------+-------+
1731 If I<format> is "billco", the fields of the detail CSV file are as follows:
1733 FORMAT FOR DETAIL FILE
1735 Field | Description | Name | Type | Width
1736 1 | N/A-Leave Empty | RC | CHAR | 2
1737 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1738 3 | Account Number | TRACCTNUM | CHAR | 15
1739 4 | Invoice Number | TRINVOICE | CHAR | 15
1740 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1741 6 | Transaction Detail | DETAILS | CHAR | 100
1742 7 | Amount | AMT | NUM* | 9
1743 8 | Line Format Control** | LNCTRL | CHAR | 2
1744 9 | Grouping Code | GROUP | CHAR | 2
1745 10 | User Defined | ACCT CODE | CHAR | 15
1750 my($self, %opt) = @_;
1752 eval "use Text::CSV_XS";
1755 my $cust_main = $self->cust_main;
1757 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1759 if ( lc($opt{'format'}) eq 'billco' ) {
1762 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1764 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1766 my( $previous_balance, @unused ) = $self->previous; #previous balance
1768 my $pmt_cr_applied = 0;
1769 $pmt_cr_applied += $_->{'amount'}
1770 foreach ( $self->_items_payments, $self->_items_credits ) ;
1772 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1775 '', # 1 | N/A-Leave Empty CHAR 2
1776 '', # 2 | N/A-Leave Empty CHAR 15
1777 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1778 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1779 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1780 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1781 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1782 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1783 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1784 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1785 '', # 10 | Ancillary Billing Information CHAR 30
1786 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1787 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1790 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1793 $duedate, # 14 | Bill Due Date CHAR 10
1795 $previous_balance, # 15 | Previous Balance NUM* 9
1796 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1797 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1798 $totaldue, # 18 | Total Amt Due NUM* 9
1799 $totaldue, # 19 | Total Amt Due NUM* 9
1800 '', # 20 | 30 Day Aging NUM* 9
1801 '', # 21 | 60 Day Aging NUM* 9
1802 '', # 22 | 90 Day Aging NUM* 9
1803 'N', # 23 | Y/N CHAR 1
1804 '', # 24 | Remittance automation CHAR 100
1805 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1806 $self->custnum, # 26 | Customer Reference Number CHAR 15
1807 '0', # 27 | Federal Tax*** NUM* 9
1808 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1809 '0', # 29 | Other Taxes & Fees*** NUM* 9
1818 time2str("%x", $self->_date),
1819 sprintf("%.2f", $self->charged),
1820 ( map { $cust_main->getfield($_) }
1821 qw( first last company address1 address2 city state zip country ) ),
1823 ) or die "can't create csv";
1826 my $header = $csv->string. "\n";
1829 if ( lc($opt{'format'}) eq 'billco' ) {
1832 foreach my $item ( $self->_items_pkg ) {
1835 '', # 1 | N/A-Leave Empty CHAR 2
1836 '', # 2 | N/A-Leave Empty CHAR 15
1837 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1838 $self->invnum, # 4 | Invoice Number CHAR 15
1839 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1840 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1841 $item->{'amount'}, # 7 | Amount NUM* 9
1842 '', # 8 | Line Format Control** CHAR 2
1843 '', # 9 | Grouping Code CHAR 2
1844 '', # 10 | User Defined CHAR 15
1847 $detail .= $csv->string. "\n";
1853 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1855 my($pkg, $setup, $recur, $sdate, $edate);
1856 if ( $cust_bill_pkg->pkgnum ) {
1858 ($pkg, $setup, $recur, $sdate, $edate) = (
1859 $cust_bill_pkg->part_pkg->pkg,
1860 ( $cust_bill_pkg->setup != 0
1861 ? sprintf("%.2f", $cust_bill_pkg->setup )
1863 ( $cust_bill_pkg->recur != 0
1864 ? sprintf("%.2f", $cust_bill_pkg->recur )
1866 ( $cust_bill_pkg->sdate
1867 ? time2str("%x", $cust_bill_pkg->sdate)
1869 ($cust_bill_pkg->edate
1870 ?time2str("%x", $cust_bill_pkg->edate)
1874 } else { #pkgnum tax
1875 next unless $cust_bill_pkg->setup != 0;
1876 $pkg = $cust_bill_pkg->desc;
1877 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1878 ( $sdate, $edate ) = ( '', '' );
1884 ( map { '' } (1..11) ),
1885 ($pkg, $setup, $recur, $sdate, $edate)
1886 ) or die "can't create csv";
1888 $detail .= $csv->string. "\n";
1894 ( $header, $detail );
1900 Pays this invoice with a compliemntary payment. If there is an error,
1901 returns the error, otherwise returns false.
1907 my $cust_pay = new FS::cust_pay ( {
1908 'invnum' => $self->invnum,
1909 'paid' => $self->owed,
1912 'payinfo' => $self->cust_main->payinfo,
1920 Attempts to pay this invoice with a credit card payment via a
1921 Business::OnlinePayment realtime gateway. See
1922 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1923 for supported processors.
1929 $self->realtime_bop( 'CC', @_ );
1934 Attempts to pay this invoice with an electronic check (ACH) payment via a
1935 Business::OnlinePayment realtime gateway. See
1936 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1937 for supported processors.
1943 $self->realtime_bop( 'ECHECK', @_ );
1948 Attempts to pay this invoice with phone bill (LEC) payment via a
1949 Business::OnlinePayment realtime gateway. See
1950 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1951 for supported processors.
1957 $self->realtime_bop( 'LEC', @_ );
1961 my( $self, $method ) = @_;
1963 my $cust_main = $self->cust_main;
1964 my $balance = $cust_main->balance;
1965 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1966 $amount = sprintf("%.2f", $amount);
1967 return "not run (balance $balance)" unless $amount > 0;
1969 my $description = 'Internet Services';
1970 if ( $conf->exists('business-onlinepayment-description') ) {
1971 my $dtempl = $conf->config('business-onlinepayment-description');
1973 my $agent_obj = $cust_main->agent
1974 or die "can't retreive agent for $cust_main (agentnum ".
1975 $cust_main->agentnum. ")";
1976 my $agent = $agent_obj->agent;
1977 my $pkgs = join(', ',
1978 map { $_->part_pkg->pkg }
1979 grep { $_->pkgnum } $self->cust_bill_pkg
1981 $description = eval qq("$dtempl");
1984 $cust_main->realtime_bop($method, $amount,
1985 'description' => $description,
1986 'invnum' => $self->invnum,
1987 #this didn't do what we want, it just calls apply_payments_and_credits
1989 'apply_to_invoice' => 1,
1991 #this changes application behavior: auto payments
1992 #triggered against a specific invoice are now applied
1993 #to that invoice instead of oldest open.
1999 =item batch_card OPTION => VALUE...
2001 Adds a payment for this invoice to the pending credit card batch (see
2002 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2003 runs the payment using a realtime gateway.
2008 my ($self, %options) = @_;
2009 my $cust_main = $self->cust_main;
2011 $options{invnum} = $self->invnum;
2013 $cust_main->batch_card(%options);
2016 sub _agent_template {
2018 $self->cust_main->agent_template;
2021 sub _agent_invoice_from {
2023 $self->cust_main->agent_invoice_from;
2026 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2028 Returns an text invoice, as a list of lines.
2030 Options can be passed as a hashref (recommended) or as a list of time, template
2031 and then any key/value pairs for any other options.
2033 I<time>, if specified, is used to control the printing of overdue messages. The
2034 default is now. It isn't the date of the invoice; that's the `_date' field.
2035 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2036 L<Time::Local> and L<Date::Parse> for conversion functions.
2038 I<template>, if specified, is the name of a suffix for alternate invoices.
2040 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2046 my( $today, $template, %opt );
2048 %opt = %{ shift() };
2049 $today = delete($opt{'time'}) || '';
2050 $template = delete($opt{template}) || '';
2052 ( $today, $template, %opt ) = @_;
2055 my %params = ( 'format' => 'template' );
2056 $params{'time'} = $today if $today;
2057 $params{'template'} = $template if $template;
2058 $params{$_} = $opt{$_}
2059 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2061 $self->print_generic( %params );
2064 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2066 Internal method - returns a filename of a filled-in LaTeX template for this
2067 invoice (Note: add ".tex" to get the actual filename), and a filename of
2068 an associated logo (with the .eps extension included).
2070 See print_ps and print_pdf for methods that return PostScript and PDF output.
2072 Options can be passed as a hashref (recommended) or as a list of time, template
2073 and then any key/value pairs for any other options.
2075 I<time>, if specified, is used to control the printing of overdue messages. The
2076 default is now. It isn't the date of the invoice; that's the `_date' field.
2077 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2078 L<Time::Local> and L<Date::Parse> for conversion functions.
2080 I<template>, if specified, is the name of a suffix for alternate invoices.
2082 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2088 my( $today, $template, %opt );
2090 %opt = %{ shift() };
2091 $today = delete($opt{'time'}) || '';
2092 $template = delete($opt{template}) || '';
2094 ( $today, $template, %opt ) = @_;
2097 my %params = ( 'format' => 'latex' );
2098 $params{'time'} = $today if $today;
2099 $params{'template'} = $template if $template;
2100 $params{$_} = $opt{$_}
2101 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2103 $template ||= $self->_agent_template;
2105 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2106 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2110 ) or die "can't open temp file: $!\n";
2112 my $agentnum = $self->cust_main->agentnum;
2114 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2115 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2116 or die "can't write temp file: $!\n";
2118 print $lh $conf->config_binary('logo.eps', $agentnum)
2119 or die "can't write temp file: $!\n";
2122 $params{'logo_file'} = $lh->filename;
2124 my @filled_in = $self->print_generic( %params );
2126 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2130 ) or die "can't open temp file: $!\n";
2131 print $fh join('', @filled_in );
2134 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2135 return ($1, $params{'logo_file'});
2139 =item print_generic OPTION => VALUE ...
2141 Internal method - returns a filled-in template for this invoice as a scalar.
2143 See print_ps and print_pdf for methods that return PostScript and PDF output.
2145 Non optional options include
2146 format - latex, html, template
2148 Optional options include
2150 template - a value used as a suffix for a configuration template
2152 time - a value used to control the printing of overdue messages. The
2153 default is now. It isn't the date of the invoice; that's the `_date' field.
2154 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2155 L<Time::Local> and L<Date::Parse> for conversion functions.
2159 unsquelch_cdr - overrides any per customer cdr squelching when true
2161 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2165 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2166 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2167 # yes: fixed width (dot matrix) text printing will be borked
2170 my( $self, %params ) = @_;
2171 my $today = $params{today} ? $params{today} : time;
2172 warn "$me print_generic called on $self with suffix $params{template}\n"
2175 my $format = $params{format};
2176 die "Unknown format: $format"
2177 unless $format =~ /^(latex|html|template)$/;
2179 my $cust_main = $self->cust_main;
2180 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2181 unless $cust_main->payname
2182 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2184 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2185 'html' => [ '<%=', '%>' ],
2186 'template' => [ '{', '}' ],
2189 warn "$me print_generic creating template\n"
2192 #create the template
2193 my $template = $params{template} ? $params{template} : $self->_agent_template;
2194 my $templatefile = "invoice_$format";
2195 $templatefile .= "_$template"
2196 if length($template);
2197 my @invoice_template = map "$_\n", $conf->config($templatefile)
2198 or die "cannot load config data $templatefile";
2201 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2202 #change this to a die when the old code is removed
2203 warn "old-style invoice template $templatefile; ".
2204 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2205 $old_latex = 'true';
2206 @invoice_template = _translate_old_latex_format(@invoice_template);
2209 warn "$me print_generic creating T:T object\n"
2212 my $text_template = new Text::Template(
2214 SOURCE => \@invoice_template,
2215 DELIMITERS => $delimiters{$format},
2218 warn "$me print_generic compiling T:T object\n"
2221 $text_template->compile()
2222 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2225 # additional substitution could possibly cause breakage in existing templates
2226 my %convert_maps = (
2228 'notes' => sub { map "$_", @_ },
2229 'footer' => sub { map "$_", @_ },
2230 'smallfooter' => sub { map "$_", @_ },
2231 'returnaddress' => sub { map "$_", @_ },
2232 'coupon' => sub { map "$_", @_ },
2233 'summary' => sub { map "$_", @_ },
2239 s/%%(.*)$/<!-- $1 -->/g;
2240 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2241 s/\\begin\{enumerate\}/<ol>/g;
2243 s/\\end\{enumerate\}/<\/ol>/g;
2244 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2253 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2255 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2260 s/\\\\\*?\s*$/<BR>/;
2261 s/\\hyphenation\{[\w\s\-]+}//;
2266 'coupon' => sub { "" },
2267 'summary' => sub { "" },
2274 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2275 s/\\begin\{enumerate\}//g;
2277 s/\\end\{enumerate\}//g;
2278 s/\\textbf\{(.*)\}/$1/g;
2285 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2287 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2292 s/\\\\\*?\s*$/\n/; # dubious
2293 s/\\hyphenation\{[\w\s\-]+}//;
2297 'coupon' => sub { "" },
2298 'summary' => sub { "" },
2303 # hashes for differing output formats
2304 my %nbsps = ( 'latex' => '~',
2305 'html' => '', # '&nbps;' would be nice
2306 'template' => '', # not used
2308 my $nbsp = $nbsps{$format};
2310 my %escape_functions = ( 'latex' => \&_latex_escape,
2311 'html' => \&_html_escape_nbsp,#\&encode_entities,
2312 'template' => sub { shift },
2314 my $escape_function = $escape_functions{$format};
2315 my $escape_function_nonbsp = ($format eq 'html')
2316 ? \&_html_escape : $escape_function;
2318 my %date_formats = ( 'latex' => $date_format_long,
2319 'html' => $date_format_long,
2322 $date_formats{'html'} =~ s/ / /g;
2324 my $date_format = $date_formats{$format};
2326 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2328 'html' => sub { return '<b>'. shift(). '</b>'
2330 'template' => sub { shift },
2332 my $embolden_function = $embolden_functions{$format};
2334 warn "$me generating template variables\n"
2337 # generate template variables
2340 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2344 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2350 $returnaddress = join("\n",
2351 $conf->config_orbase("invoice_${format}returnaddress", $template)
2354 } elsif ( grep /\S/,
2355 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2357 my $convert_map = $convert_maps{$format}{'returnaddress'};
2360 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2365 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2367 my $convert_map = $convert_maps{$format}{'returnaddress'};
2368 $returnaddress = join( "\n", &$convert_map(
2369 map { s/( {2,})/'~' x length($1)/eg;
2373 ( $conf->config('company_name', $self->cust_main->agentnum),
2374 $conf->config('company_address', $self->cust_main->agentnum),
2381 my $warning = "Couldn't find a return address; ".
2382 "do you need to set the company_address configuration value?";
2384 $returnaddress = $nbsp;
2385 #$returnaddress = $warning;
2389 warn "$me generating invoice data\n"
2392 my $agentnum = $self->cust_main->agentnum;
2394 my %invoice_data = (
2397 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2398 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2399 'returnaddress' => $returnaddress,
2400 'agent' => &$escape_function($cust_main->agent->agent),
2403 'invnum' => $self->invnum,
2404 'date' => time2str($date_format, $self->_date),
2405 'today' => time2str($date_format_long, $today),
2406 'terms' => $self->terms,
2407 'template' => $template, #params{'template'},
2408 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2409 'current_charges' => sprintf("%.2f", $self->charged),
2410 'duedate' => $self->due_date2str($rdate_format), #date_format?
2413 'custnum' => $cust_main->display_custnum,
2414 'agent_custid' => &$escape_function($cust_main->agent_custid),
2415 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2416 payname company address1 address2 city state zip fax
2420 'ship_enable' => $conf->exists('invoice-ship_address'),
2421 'unitprices' => $conf->exists('invoice-unitprice'),
2422 'smallernotes' => $conf->exists('invoice-smallernotes'),
2423 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2424 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2426 #layout info -- would be fancy to calc some of this and bury the template
2428 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2429 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2430 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2431 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2432 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2433 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2434 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2435 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2436 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2437 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2439 # better hang on to conf_dir for a while (for old templates)
2440 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2442 #these are only used when doing paged plaintext
2448 $invoice_data{finance_section} = '';
2449 if ( $conf->config('finance_pkgclass') ) {
2451 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2452 $invoice_data{finance_section} = $pkg_class->categoryname;
2454 $invoice_data{finance_amount} = '0.00';
2455 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2457 my $countrydefault = $conf->config('countrydefault') || 'US';
2458 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2459 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2460 my $method = $prefix.$_;
2461 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2463 $invoice_data{'ship_country'} = ''
2464 if ( $invoice_data{'ship_country'} eq $countrydefault );
2466 $invoice_data{'cid'} = $params{'cid'}
2469 if ( $cust_main->country eq $countrydefault ) {
2470 $invoice_data{'country'} = '';
2472 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2476 $invoice_data{'address'} = \@address;
2478 $cust_main->payname.
2479 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2480 ? " (P.O. #". $cust_main->payinfo. ")"
2484 push @address, $cust_main->company
2485 if $cust_main->company;
2486 push @address, $cust_main->address1;
2487 push @address, $cust_main->address2
2488 if $cust_main->address2;
2490 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2491 push @address, $invoice_data{'country'}
2492 if $invoice_data{'country'};
2494 while (scalar(@address) < 5);
2496 $invoice_data{'logo_file'} = $params{'logo_file'}
2497 if $params{'logo_file'};
2499 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2500 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2501 #my $balance_due = $self->owed + $pr_total - $cr_total;
2502 my $balance_due = $self->owed + $pr_total;
2503 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2504 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2505 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2506 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2508 my $summarypage = '';
2509 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2512 $invoice_data{'summarypage'} = $summarypage;
2514 warn "$me substituting variables in notes, footer, smallfooter\n"
2517 foreach my $include (qw( notes footer smallfooter coupon )) {
2519 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2522 if ( $conf->exists($inc_file, $agentnum)
2523 && length( $conf->config($inc_file, $agentnum) ) ) {
2525 @inc_src = $conf->config($inc_file, $agentnum);
2529 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2531 my $convert_map = $convert_maps{$format}{$include};
2533 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2534 s/--\@\]/$delimiters{$format}[1]/g;
2537 &$convert_map( $conf->config($inc_file, $agentnum) );
2541 my $inc_tt = new Text::Template (
2543 SOURCE => [ map "$_\n", @inc_src ],
2544 DELIMITERS => $delimiters{$format},
2545 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2547 unless ( $inc_tt->compile() ) {
2548 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2549 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2553 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2555 $invoice_data{$include} =~ s/\n+$//
2556 if ($format eq 'latex');
2559 $invoice_data{'po_line'} =
2560 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2561 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2564 my %money_chars = ( 'latex' => '',
2565 'html' => $conf->config('money_char') || '$',
2568 my $money_char = $money_chars{$format};
2570 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2571 'html' => $conf->config('money_char') || '$',
2574 my $other_money_char = $other_money_chars{$format};
2575 $invoice_data{'dollar'} = $other_money_char;
2577 my @detail_items = ();
2578 my @total_items = ();
2582 $invoice_data{'detail_items'} = \@detail_items;
2583 $invoice_data{'total_items'} = \@total_items;
2584 $invoice_data{'buf'} = \@buf;
2585 $invoice_data{'sections'} = \@sections;
2587 warn "$me generating sections\n"
2590 my $previous_section = { 'description' => 'Previous Charges',
2591 'subtotal' => $other_money_char.
2592 sprintf('%.2f', $pr_total),
2593 'summarized' => $summarypage ? 'Y' : '',
2595 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2596 join(' / ', map { $cust_main->balance_date_range(@$_) }
2597 $self->_prior_month30s
2599 if $conf->exists('invoice_include_aging');
2602 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2603 'subtotal' => $taxtotal, # adjusted below
2604 'summarized' => $summarypage ? 'Y' : '',
2606 my $tax_weight = _pkg_category($tax_section->{description})
2607 ? _pkg_category($tax_section->{description})->weight
2609 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2610 $tax_section->{'sort_weight'} = $tax_weight;
2613 my $adjusttotal = 0;
2614 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2615 'subtotal' => 0, # adjusted below
2616 'summarized' => $summarypage ? 'Y' : '',
2618 my $adjust_weight = _pkg_category($adjust_section->{description})
2619 ? _pkg_category($adjust_section->{description})->weight
2621 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2622 $adjust_section->{'sort_weight'} = $adjust_weight;
2624 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2625 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2626 $invoice_data{'multisection'} = $multisection;
2627 my $late_sections = [];
2628 my $extra_sections = [];
2629 my $extra_lines = ();
2630 if ( $multisection ) {
2631 ($extra_sections, $extra_lines) =
2632 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2633 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2635 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2637 push @detail_items, @$extra_lines if $extra_lines;
2639 $self->_items_sections( $late_sections, # this could stand a refactor
2641 $escape_function_nonbsp,
2645 if ($conf->exists('svc_phone_sections')) {
2646 my ($phone_sections, $phone_lines) =
2647 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2648 push @{$late_sections}, @$phone_sections;
2649 push @detail_items, @$phone_lines;
2652 push @sections, { 'description' => '', 'subtotal' => '' };
2655 unless ( $conf->exists('disable_previous_balance')
2656 || $conf->exists('previous_balance-summary_only')
2660 warn "$me adding previous balances\n"
2663 foreach my $line_item ( $self->_items_previous ) {
2666 ext_description => [],
2668 $detail->{'ref'} = $line_item->{'pkgnum'};
2669 $detail->{'quantity'} = 1;
2670 $detail->{'section'} = $previous_section;
2671 $detail->{'description'} = &$escape_function($line_item->{'description'});
2672 if ( exists $line_item->{'ext_description'} ) {
2673 @{$detail->{'ext_description'}} = map {
2674 &$escape_function($_);
2675 } @{$line_item->{'ext_description'}};
2677 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2678 $line_item->{'amount'};
2679 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2681 push @detail_items, $detail;
2682 push @buf, [ $detail->{'description'},
2683 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2689 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2690 push @buf, ['','-----------'];
2691 push @buf, [ 'Total Previous Balance',
2692 $money_char. sprintf("%10.2f", $pr_total) ];
2696 if ( $conf->exists('svc_phone-did-summary') ) {
2697 warn "$me adding DID summary\n"
2700 my ($didsummary,$minutes) = $self->_did_summary;
2701 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2703 { 'description' => $didsummary_desc,
2704 'ext_description' => [ $didsummary, $minutes ],
2709 foreach my $section (@sections, @$late_sections) {
2711 warn "$me adding section \n". Dumper($section)
2714 # begin some normalization
2715 $section->{'subtotal'} = $section->{'amount'}
2717 && !exists($section->{subtotal})
2718 && exists($section->{amount});
2720 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2721 if ( $invoice_data{finance_section} &&
2722 $section->{'description'} eq $invoice_data{finance_section} );
2724 $section->{'subtotal'} = $other_money_char.
2725 sprintf('%.2f', $section->{'subtotal'})
2728 # continue some normalization
2729 $section->{'amount'} = $section->{'subtotal'}
2733 if ( $section->{'description'} ) {
2734 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2739 warn "$me setting options\n"
2742 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2744 $options{'section'} = $section if $multisection;
2745 $options{'format'} = $format;
2746 $options{'escape_function'} = $escape_function;
2747 $options{'format_function'} = sub { () } unless $unsquelched;
2748 $options{'unsquelched'} = $unsquelched;
2749 $options{'summary_page'} = $summarypage;
2750 $options{'skip_usage'} =
2751 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2752 $options{'multilocation'} = $multilocation;
2753 $options{'multisection'} = $multisection;
2755 warn "$me searching for line items\n"
2758 foreach my $line_item ( $self->_items_pkg(%options) ) {
2760 warn "$me adding line item $line_item\n"
2764 ext_description => [],
2766 $detail->{'ref'} = $line_item->{'pkgnum'};
2767 $detail->{'quantity'} = $line_item->{'quantity'};
2768 $detail->{'section'} = $section;
2769 $detail->{'description'} = &$escape_function($line_item->{'description'});
2770 if ( exists $line_item->{'ext_description'} ) {
2771 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2773 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2774 $line_item->{'amount'};
2775 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2776 $line_item->{'unit_amount'};
2777 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2779 push @detail_items, $detail;
2780 push @buf, ( [ $detail->{'description'},
2781 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2783 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2787 if ( $section->{'description'} ) {
2788 push @buf, ( ['','-----------'],
2789 [ $section->{'description'}. ' sub-total',
2790 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2799 $invoice_data{current_less_finance} =
2800 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2802 if ( $multisection && !$conf->exists('disable_previous_balance')
2803 || $conf->exists('previous_balance-summary_only') )
2805 unshift @sections, $previous_section if $pr_total;
2808 warn "$me adding taxes\n"
2811 foreach my $tax ( $self->_items_tax ) {
2813 $taxtotal += $tax->{'amount'};
2815 my $description = &$escape_function( $tax->{'description'} );
2816 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2818 if ( $multisection ) {
2820 my $money = $old_latex ? '' : $money_char;
2821 push @detail_items, {
2822 ext_description => [],
2825 description => $description,
2826 amount => $money. $amount,
2828 section => $tax_section,
2833 push @total_items, {
2834 'total_item' => $description,
2835 'total_amount' => $other_money_char. $amount,
2840 push @buf,[ $description,
2841 $money_char. $amount,
2848 $total->{'total_item'} = 'Sub-total';
2849 $total->{'total_amount'} =
2850 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2852 if ( $multisection ) {
2853 $tax_section->{'subtotal'} = $other_money_char.
2854 sprintf('%.2f', $taxtotal);
2855 $tax_section->{'pretotal'} = 'New charges sub-total '.
2856 $total->{'total_amount'};
2857 push @sections, $tax_section if $taxtotal;
2859 unshift @total_items, $total;
2862 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2864 push @buf,['','-----------'];
2865 push @buf,[( $conf->exists('disable_previous_balance')
2867 : 'Total New Charges'
2869 $money_char. sprintf("%10.2f",$self->charged) ];
2875 $item = $conf->config('previous_balance-exclude_from_total')
2876 || 'Total New Charges'
2877 if $conf->exists('previous_balance-exclude_from_total');
2878 my $amount = $self->charged +
2879 ( $conf->exists('disable_previous_balance') ||
2880 $conf->exists('previous_balance-exclude_from_total')
2884 $total->{'total_item'} = &$embolden_function($item);
2885 $total->{'total_amount'} =
2886 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2887 if ( $multisection ) {
2888 if ( $adjust_section->{'sort_weight'} ) {
2889 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2890 sprintf("%.2f", ($self->billing_balance || 0) );
2892 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2893 sprintf('%.2f', $self->charged );
2896 push @total_items, $total;
2898 push @buf,['','-----------'];
2901 sprintf( '%10.2f', $amount )
2906 unless ( $conf->exists('disable_previous_balance') ) {
2907 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2910 my $credittotal = 0;
2911 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2914 $total->{'total_item'} = &$escape_function($credit->{'description'});
2915 $credittotal += $credit->{'amount'};
2916 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2917 $adjusttotal += $credit->{'amount'};
2918 if ( $multisection ) {
2919 my $money = $old_latex ? '' : $money_char;
2920 push @detail_items, {
2921 ext_description => [],
2924 description => &$escape_function($credit->{'description'}),
2925 amount => $money. $credit->{'amount'},
2927 section => $adjust_section,
2930 push @total_items, $total;
2934 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2937 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2938 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2942 my $paymenttotal = 0;
2943 foreach my $payment ( $self->_items_payments ) {
2945 $total->{'total_item'} = &$escape_function($payment->{'description'});
2946 $paymenttotal += $payment->{'amount'};
2947 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2948 $adjusttotal += $payment->{'amount'};
2949 if ( $multisection ) {
2950 my $money = $old_latex ? '' : $money_char;
2951 push @detail_items, {
2952 ext_description => [],
2955 description => &$escape_function($payment->{'description'}),
2956 amount => $money. $payment->{'amount'},
2958 section => $adjust_section,
2961 push @total_items, $total;
2963 push @buf, [ $payment->{'description'},
2964 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2967 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2969 if ( $multisection ) {
2970 $adjust_section->{'subtotal'} = $other_money_char.
2971 sprintf('%.2f', $adjusttotal);
2972 push @sections, $adjust_section
2973 unless $adjust_section->{sort_weight};
2978 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2979 $total->{'total_amount'} =
2980 &$embolden_function(
2981 $other_money_char. sprintf('%.2f', $summarypage
2983 $self->billing_balance
2984 : $self->owed + $pr_total
2987 if ( $multisection && !$adjust_section->{sort_weight} ) {
2988 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2989 $total->{'total_amount'};
2991 push @total_items, $total;
2993 push @buf,['','-----------'];
2994 push @buf,[$self->balance_due_msg, $money_char.
2995 sprintf("%10.2f", $balance_due ) ];
2999 if ( $multisection ) {
3000 if ($conf->exists('svc_phone_sections')) {
3002 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3003 $total->{'total_amount'} =
3004 &$embolden_function(
3005 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3007 my $last_section = pop @sections;
3008 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3009 $total->{'total_amount'};
3010 push @sections, $last_section;
3012 push @sections, @$late_sections
3016 my @includelist = ();
3017 push @includelist, 'summary' if $summarypage;
3018 foreach my $include ( @includelist ) {
3020 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3023 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3025 @inc_src = $conf->config($inc_file, $agentnum);
3029 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3031 my $convert_map = $convert_maps{$format}{$include};
3033 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3034 s/--\@\]/$delimiters{$format}[1]/g;
3037 &$convert_map( $conf->config($inc_file, $agentnum) );
3041 my $inc_tt = new Text::Template (
3043 SOURCE => [ map "$_\n", @inc_src ],
3044 DELIMITERS => $delimiters{$format},
3045 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3047 unless ( $inc_tt->compile() ) {
3048 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3049 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3053 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3055 $invoice_data{$include} =~ s/\n+$//
3056 if ($format eq 'latex');
3061 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3062 /invoice_lines\((\d*)\)/;
3063 $invoice_lines += $1 || scalar(@buf);
3066 die "no invoice_lines() functions in template?"
3067 if ( $format eq 'template' && !$wasfunc );
3069 if ($format eq 'template') {
3071 if ( $invoice_lines ) {
3072 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3073 $invoice_data{'total_pages'}++
3074 if scalar(@buf) % $invoice_lines;
3077 #setup subroutine for the template
3078 sub FS::cust_bill::_template::invoice_lines {
3079 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3081 scalar(@FS::cust_bill::_template::buf)
3082 ? shift @FS::cust_bill::_template::buf
3091 push @collect, split("\n",
3092 $text_template->fill_in( HASH => \%invoice_data,
3093 PACKAGE => 'FS::cust_bill::_template'
3096 $FS::cust_bill::_template::page++;
3098 map "$_\n", @collect;
3100 warn "filling in template for invoice ". $self->invnum. "\n"
3102 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3105 $text_template->fill_in(HASH => \%invoice_data);
3109 # helper routine for generating date ranges
3110 sub _prior_month30s {
3113 [ 1, 2592000 ], # 0-30 days ago
3114 [ 2592000, 5184000 ], # 30-60 days ago
3115 [ 5184000, 7776000 ], # 60-90 days ago
3116 [ 7776000, 0 ], # 90+ days ago
3119 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3120 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3125 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3127 Returns an postscript invoice, as a scalar.
3129 Options can be passed as a hashref (recommended) or as a list of time, template
3130 and then any key/value pairs for any other options.
3132 I<time> an optional value used to control the printing of overdue messages. The
3133 default is now. It isn't the date of the invoice; that's the `_date' field.
3134 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3135 L<Time::Local> and L<Date::Parse> for conversion functions.
3137 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3144 my ($file, $lfile) = $self->print_latex(@_);
3145 my $ps = generate_ps($file);
3151 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3153 Returns an PDF invoice, as a scalar.
3155 Options can be passed as a hashref (recommended) or as a list of time, template
3156 and then any key/value pairs for any other options.
3158 I<time> an optional value used to control the printing of overdue messages. The
3159 default is now. It isn't the date of the invoice; that's the `_date' field.
3160 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3161 L<Time::Local> and L<Date::Parse> for conversion functions.
3163 I<template>, if specified, is the name of a suffix for alternate invoices.
3165 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3172 my ($file, $lfile) = $self->print_latex(@_);
3173 my $pdf = generate_pdf($file);
3179 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3181 Returns an HTML invoice, as a scalar.
3183 I<time> an optional value used to control the printing of overdue messages. The
3184 default is now. It isn't the date of the invoice; that's the `_date' field.
3185 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3186 L<Time::Local> and L<Date::Parse> for conversion functions.
3188 I<template>, if specified, is the name of a suffix for alternate invoices.
3190 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3192 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3193 when emailing the invoice as part of a multipart/related MIME email.
3201 %params = %{ shift() };
3203 $params{'time'} = shift;
3204 $params{'template'} = shift;
3205 $params{'cid'} = shift;
3208 $params{'format'} = 'html';
3210 $self->print_generic( %params );
3213 # quick subroutine for print_latex
3215 # There are ten characters that LaTeX treats as special characters, which
3216 # means that they do not simply typeset themselves:
3217 # # $ % & ~ _ ^ \ { }
3219 # TeX ignores blanks following an escaped character; if you want a blank (as
3220 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3224 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3225 $value =~ s/([<>])/\$$1\$/g;
3231 encode_entities($value);
3235 sub _html_escape_nbsp {
3236 my $value = _html_escape(shift);
3237 $value =~ s/ +/ /g;
3241 #utility methods for print_*
3243 sub _translate_old_latex_format {
3244 warn "_translate_old_latex_format called\n"
3251 if ( $line =~ /^%%Detail\s*$/ ) {
3253 push @template, q![@--!,
3254 q! foreach my $_tr_line (@detail_items) {!,
3255 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3256 q! $_tr_line->{'description'} .= !,
3257 q! "\\tabularnewline\n~~".!,
3258 q! join( "\\tabularnewline\n~~",!,
3259 q! @{$_tr_line->{'ext_description'}}!,
3263 while ( ( my $line_item_line = shift )
3264 !~ /^%%EndDetail\s*$/ ) {
3265 $line_item_line =~ s/'/\\'/g; # nice LTS
3266 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3267 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3268 push @template, " \$OUT .= '$line_item_line';";
3271 push @template, '}',
3274 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3276 push @template, '[@--',
3277 ' foreach my $_tr_line (@total_items) {';
3279 while ( ( my $total_item_line = shift )
3280 !~ /^%%EndTotalDetails\s*$/ ) {
3281 $total_item_line =~ s/'/\\'/g; # nice LTS
3282 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3283 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3284 push @template, " \$OUT .= '$total_item_line';";
3287 push @template, '}',
3291 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3292 push @template, $line;
3298 warn "$_\n" foreach @template;
3307 #check for an invoice-specific override
3308 return $self->invoice_terms if $self->invoice_terms;
3310 #check for a customer- specific override
3311 my $cust_main = $self->cust_main;
3312 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3314 #use configured default
3315 $conf->config('invoice_default_terms') || '';
3321 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3322 $duedate = $self->_date() + ( $1 * 86400 );
3329 $self->due_date ? time2str(shift, $self->due_date) : '';
3332 sub balance_due_msg {
3334 my $msg = 'Balance Due';
3335 return $msg unless $self->terms;
3336 if ( $self->due_date ) {
3337 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3338 } elsif ( $self->terms ) {
3339 $msg .= ' - '. $self->terms;
3344 sub balance_due_date {
3347 if ( $conf->exists('invoice_default_terms')
3348 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3349 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3354 =item invnum_date_pretty
3356 Returns a string with the invoice number and date, for example:
3357 "Invoice #54 (3/20/2008)"
3361 sub invnum_date_pretty {
3363 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3368 Returns a string with the date, for example: "3/20/2008"
3374 time2str($date_format, $self->_date);
3377 use vars qw(%pkg_category_cache);
3378 sub _items_sections {
3381 my $summarypage = shift;
3383 my $extra_sections = shift;
3387 my %late_subtotal = ();
3390 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3393 my $usage = $cust_bill_pkg->usage;
3395 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3396 next if ( $display->summary && $summarypage );
3398 my $section = $display->section;
3399 my $type = $display->type;
3401 $not_tax{$section} = 1
3402 unless $cust_bill_pkg->pkgnum == 0;
3404 if ( $display->post_total && !$summarypage ) {
3405 if (! $type || $type eq 'S') {
3406 $late_subtotal{$section} += $cust_bill_pkg->setup
3407 if $cust_bill_pkg->setup != 0;
3411 $late_subtotal{$section} += $cust_bill_pkg->recur
3412 if $cust_bill_pkg->recur != 0;
3415 if ($type && $type eq 'R') {
3416 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3417 if $cust_bill_pkg->recur != 0;
3420 if ($type && $type eq 'U') {
3421 $late_subtotal{$section} += $usage
3422 unless scalar(@$extra_sections);
3427 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3429 if (! $type || $type eq 'S') {
3430 $subtotal{$section} += $cust_bill_pkg->setup
3431 if $cust_bill_pkg->setup != 0;
3435 $subtotal{$section} += $cust_bill_pkg->recur
3436 if $cust_bill_pkg->recur != 0;
3439 if ($type && $type eq 'R') {
3440 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3441 if $cust_bill_pkg->recur != 0;
3444 if ($type && $type eq 'U') {
3445 $subtotal{$section} += $usage
3446 unless scalar(@$extra_sections);
3455 %pkg_category_cache = ();
3457 push @$late, map { { 'description' => &{$escape}($_),
3458 'subtotal' => $late_subtotal{$_},
3460 'sort_weight' => ( _pkg_category($_)
3461 ? _pkg_category($_)->weight
3464 ((_pkg_category($_) && _pkg_category($_)->condense)
3465 ? $self->_condense_section($format)
3469 sort _sectionsort keys %late_subtotal;
3472 if ( $summarypage ) {
3473 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3474 map { $_->categoryname } qsearch('pkg_category', {});
3475 push @sections, '' if exists($subtotal{''});
3477 @sections = keys %subtotal;
3480 my @early = map { { 'description' => &{$escape}($_),
3481 'subtotal' => $subtotal{$_},
3482 'summarized' => $not_tax{$_} ? '' : 'Y',
3483 'tax_section' => $not_tax{$_} ? '' : 'Y',
3484 'sort_weight' => ( _pkg_category($_)
3485 ? _pkg_category($_)->weight
3488 ((_pkg_category($_) && _pkg_category($_)->condense)
3489 ? $self->_condense_section($format)
3494 push @early, @$extra_sections if $extra_sections;
3496 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3500 #helper subs for above
3503 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3507 my $categoryname = shift;
3508 $pkg_category_cache{$categoryname} ||=
3509 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3512 my %condensed_format = (
3513 'label' => [ qw( Description Qty Amount ) ],
3515 sub { shift->{description} },
3516 sub { shift->{quantity} },
3517 sub { my($href, %opt) = @_;
3518 ($opt{dollar} || ''). $href->{amount};
3521 'align' => [ qw( l r r ) ],
3522 'span' => [ qw( 5 1 1 ) ], # unitprices?
3523 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3526 sub _condense_section {
3527 my ( $self, $format ) = ( shift, shift );
3529 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3530 qw( description_generator
3533 total_line_generator
3538 sub _condensed_generator_defaults {
3539 my ( $self, $format ) = ( shift, shift );
3540 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3549 sub _condensed_header_generator {
3550 my ( $self, $format ) = ( shift, shift );
3552 my ( $f, $prefix, $suffix, $separator, $column ) =
3553 _condensed_generator_defaults($format);
3555 if ($format eq 'latex') {
3556 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3557 $suffix = "\\\\\n\\hline";
3560 sub { my ($d,$a,$s,$w) = @_;
3561 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3563 } elsif ( $format eq 'html' ) {
3564 $prefix = '<th></th>';
3568 sub { my ($d,$a,$s,$w) = @_;
3569 return qq!<th align="$html_align{$a}">$d</th>!;
3577 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3579 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3582 $prefix. join($separator, @result). $suffix;
3587 sub _condensed_description_generator {
3588 my ( $self, $format ) = ( shift, shift );
3590 my ( $f, $prefix, $suffix, $separator, $column ) =
3591 _condensed_generator_defaults($format);
3593 my $money_char = '$';
3594 if ($format eq 'latex') {
3595 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3597 $separator = " & \n";
3599 sub { my ($d,$a,$s,$w) = @_;
3600 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3602 $money_char = '\\dollar';
3603 }elsif ( $format eq 'html' ) {
3604 $prefix = '"><td align="center"></td>';
3608 sub { my ($d,$a,$s,$w) = @_;
3609 return qq!<td align="$html_align{$a}">$d</td>!;
3611 #$money_char = $conf->config('money_char') || '$';
3612 $money_char = ''; # this is madness
3620 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3622 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3624 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3625 map { $f->{$_}->[$i] } qw(align span width)
3629 $prefix. join( $separator, @result ). $suffix;
3634 sub _condensed_total_generator {
3635 my ( $self, $format ) = ( shift, shift );
3637 my ( $f, $prefix, $suffix, $separator, $column ) =
3638 _condensed_generator_defaults($format);
3641 if ($format eq 'latex') {
3644 $separator = " & \n";
3646 sub { my ($d,$a,$s,$w) = @_;
3647 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3649 }elsif ( $format eq 'html' ) {
3653 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3655 sub { my ($d,$a,$s,$w) = @_;
3656 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3665 # my $r = &{$f->{fields}->[$i]}(@args);
3666 # $r .= ' Total' unless $i;
3668 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3670 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3671 map { $f->{$_}->[$i] } qw(align span width)
3675 $prefix. join( $separator, @result ). $suffix;
3680 =item total_line_generator FORMAT
3682 Returns a coderef used for generation of invoice total line items for this
3683 usage_class. FORMAT is either html or latex
3687 # should not be used: will have issues with hash element names (description vs
3688 # total_item and amount vs total_amount -- another array of functions?
3690 sub _condensed_total_line_generator {
3691 my ( $self, $format ) = ( shift, shift );
3693 my ( $f, $prefix, $suffix, $separator, $column ) =
3694 _condensed_generator_defaults($format);
3697 if ($format eq 'latex') {
3700 $separator = " & \n";
3702 sub { my ($d,$a,$s,$w) = @_;
3703 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3705 }elsif ( $format eq 'html' ) {
3709 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3711 sub { my ($d,$a,$s,$w) = @_;
3712 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3721 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3723 &{$column}( &{$f->{fields}->[$i]}(@args),
3724 map { $f->{$_}->[$i] } qw(align span width)
3728 $prefix. join( $separator, @result ). $suffix;
3733 #sub _items_extra_usage_sections {
3735 # my $escape = shift;
3737 # my %sections = ();
3739 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3740 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3742 # next unless $cust_bill_pkg->pkgnum > 0;
3744 # foreach my $section ( keys %usage_class ) {
3746 # my $usage = $cust_bill_pkg->usage($section);
3748 # next unless $usage && $usage > 0;
3750 # $sections{$section} ||= 0;
3751 # $sections{$section} += $usage;
3757 # map { { 'description' => &{$escape}($_),
3758 # 'subtotal' => $sections{$_},
3759 # 'summarized' => '',
3760 # 'tax_section' => '',
3763 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3767 sub _items_extra_usage_sections {
3776 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3777 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3778 next unless $cust_bill_pkg->pkgnum > 0;
3780 foreach my $classnum ( keys %usage_class ) {
3781 my $section = $usage_class{$classnum}->classname;
3782 $classnums{$section} = $classnum;
3784 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3785 my $amount = $detail->amount;
3786 next unless $amount && $amount > 0;
3788 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3789 $sections{$section}{amount} += $amount; #subtotal
3790 $sections{$section}{calls}++;
3791 $sections{$section}{duration} += $detail->duration;
3793 my $desc = $detail->regionname;
3794 my $description = $desc;
3795 $description = substr($desc, 0, 50). '...'
3796 if $format eq 'latex' && length($desc) > 50;
3798 $lines{$section}{$desc} ||= {
3799 description => &{$escape}($description),
3800 #pkgpart => $part_pkg->pkgpart,
3801 pkgnum => $cust_bill_pkg->pkgnum,
3806 #unit_amount => $cust_bill_pkg->unitrecur,
3807 quantity => $cust_bill_pkg->quantity,
3808 product_code => 'N/A',
3809 ext_description => [],
3812 $lines{$section}{$desc}{amount} += $amount;
3813 $lines{$section}{$desc}{calls}++;
3814 $lines{$section}{$desc}{duration} += $detail->duration;
3820 my %sectionmap = ();
3821 foreach (keys %sections) {
3822 my $usage_class = $usage_class{$classnums{$_}};
3823 $sectionmap{$_} = { 'description' => &{$escape}($_),
3824 'amount' => $sections{$_}{amount}, #subtotal
3825 'calls' => $sections{$_}{calls},
3826 'duration' => $sections{$_}{duration},
3828 'tax_section' => '',
3829 'sort_weight' => $usage_class->weight,
3830 ( $usage_class->format
3831 ? ( map { $_ => $usage_class->$_($format) }
3832 qw( description_generator header_generator total_generator total_line_generator )
3839 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3843 foreach my $section ( keys %lines ) {
3844 foreach my $line ( keys %{$lines{$section}} ) {
3845 my $l = $lines{$section}{$line};
3846 $l->{section} = $sectionmap{$section};
3847 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3848 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3853 return(\@sections, \@lines);
3859 my $end = $self->_date;
3860 my $start = $end - 2592000; # 30 days
3861 my $cust_main = $self->cust_main;
3862 my @pkgs = $cust_main->all_pkgs;
3863 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3866 foreach my $pkg ( @pkgs ) {
3867 my @h_cust_svc = $pkg->h_cust_svc($end);
3868 foreach my $h_cust_svc ( @h_cust_svc ) {
3869 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3870 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3872 my $inserted = $h_cust_svc->date_inserted;
3873 my $deleted = $h_cust_svc->date_deleted;
3874 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3876 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3878 # DID either activated or ported in; cannot be both for same DID simultaneously
3879 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3880 && (!$phone_inserted->lnp_status
3881 || $phone_inserted->lnp_status eq ''
3882 || $phone_inserted->lnp_status eq 'native')) {
3885 else { # this one not so clean, should probably move to (h_)svc_phone
3886 my $phone_portedin = qsearchs( 'h_svc_phone',
3887 { 'svcnum' => $h_cust_svc->svcnum,
3888 'lnp_status' => 'portedin' },
3889 FS::h_svc_phone->sql_h_searchs($end),
3891 $num_portedin++ if $phone_portedin;
3894 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3895 if($deleted >= $start && $deleted <= $end && $phone_deleted
3896 && (!$phone_deleted->lnp_status
3897 || $phone_deleted->lnp_status ne 'portingout')) {
3900 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3901 && $phone_deleted->lnp_status
3902 && $phone_deleted->lnp_status eq 'portingout') {
3906 # increment usage minutes
3907 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3908 foreach my $cdr ( @cdrs ) {
3909 $minutes += $cdr->billsec/60;
3912 # don't look at this service again
3913 push @seen, $h_cust_svc->svcnum;
3917 $minutes = sprintf("%d", $minutes);
3918 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3919 . "$num_deactivated Ported-Out: $num_portedout ",
3920 "Total Minutes: $minutes");
3923 sub _items_svc_phone_sections {
3932 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3933 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3935 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3936 next unless $cust_bill_pkg->pkgnum > 0;
3938 my @header = $cust_bill_pkg->details_header;
3939 next unless scalar(@header);
3941 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3943 my $phonenum = $detail->phonenum;
3944 next unless $phonenum;
3946 my $amount = $detail->amount;
3947 next unless $amount && $amount > 0;
3949 $sections{$phonenum} ||= { 'amount' => 0,
3952 'sort_weight' => -1,
3953 'phonenum' => $phonenum,
3955 $sections{$phonenum}{amount} += $amount; #subtotal
3956 $sections{$phonenum}{calls}++;
3957 $sections{$phonenum}{duration} += $detail->duration;
3959 my $desc = $detail->regionname;
3960 my $description = $desc;
3961 $description = substr($desc, 0, 50). '...'
3962 if $format eq 'latex' && length($desc) > 50;
3964 $lines{$phonenum}{$desc} ||= {
3965 description => &{$escape}($description),
3966 #pkgpart => $part_pkg->pkgpart,
3974 product_code => 'N/A',
3975 ext_description => [],
3978 $lines{$phonenum}{$desc}{amount} += $amount;
3979 $lines{$phonenum}{$desc}{calls}++;
3980 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3982 my $line = $usage_class{$detail->classnum}->classname;
3983 $sections{"$phonenum $line"} ||=
3987 'sort_weight' => $usage_class{$detail->classnum}->weight,
3988 'phonenum' => $phonenum,
3989 'header' => [ @header ],
3991 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3992 $sections{"$phonenum $line"}{calls}++;
3993 $sections{"$phonenum $line"}{duration} += $detail->duration;
3995 $lines{"$phonenum $line"}{$desc} ||= {
3996 description => &{$escape}($description),
3997 #pkgpart => $part_pkg->pkgpart,
4005 product_code => 'N/A',
4006 ext_description => [],
4009 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4010 $lines{"$phonenum $line"}{$desc}{calls}++;
4011 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4012 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4013 $detail->formatted('format' => $format);
4018 my %sectionmap = ();
4019 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4020 foreach ( keys %sections ) {
4021 my @header = @{ $sections{$_}{header} || [] };
4023 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4024 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4025 my $usage_class = $summary ? $simple : $usage_simple;
4026 my $ending = $summary ? ' usage charges' : '';
4029 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4031 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4032 'amount' => $sections{$_}{amount}, #subtotal
4033 'calls' => $sections{$_}{calls},
4034 'duration' => $sections{$_}{duration},
4036 'tax_section' => '',
4037 'phonenum' => $sections{$_}{phonenum},
4038 'sort_weight' => $sections{$_}{sort_weight},
4039 'post_total' => $summary, #inspire pagebreak
4041 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4042 qw( description_generator
4045 total_line_generator
4052 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4053 $a->{sort_weight} <=> $b->{sort_weight}
4058 foreach my $section ( keys %lines ) {
4059 foreach my $line ( keys %{$lines{$section}} ) {
4060 my $l = $lines{$section}{$line};
4061 $l->{section} = $sectionmap{$section};
4062 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4063 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4068 return(\@sections, \@lines);
4075 #my @display = scalar(@_)
4077 # : qw( _items_previous _items_pkg );
4078 # #: qw( _items_pkg );
4079 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4080 my @display = qw( _items_previous _items_pkg );
4083 foreach my $display ( @display ) {
4084 push @b, $self->$display(@_);
4089 sub _items_previous {
4091 my $cust_main = $self->cust_main;
4092 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4094 foreach ( @pr_cust_bill ) {
4095 my $date = $conf->exists('invoice_show_prior_due_date')
4096 ? 'due '. $_->due_date2str($date_format)
4097 : time2str($date_format, $_->_date);
4099 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4100 #'pkgpart' => 'N/A',
4102 'amount' => sprintf("%.2f", $_->owed),
4108 # 'description' => 'Previous Balance',
4109 # #'pkgpart' => 'N/A',
4110 # 'pkgnum' => 'N/A',
4111 # 'amount' => sprintf("%10.2f", $pr_total ),
4112 # 'ext_description' => [ map {
4113 # "Invoice ". $_->invnum.
4114 # " (". time2str("%x",$_->_date). ") ".
4115 # sprintf("%10.2f", $_->owed)
4116 # } @pr_cust_bill ],
4125 warn "$me _items_pkg searching for all package line items\n"
4128 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4130 warn "$me _items_pkg filtering line items\n"
4132 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4134 if ($options{section} && $options{section}->{condensed}) {
4136 warn "$me _items_pkg condensing section\n"
4140 local $Storable::canonical = 1;
4141 foreach ( @items ) {
4143 delete $item->{ref};
4144 delete $item->{ext_description};
4145 my $key = freeze($item);
4146 $itemshash{$key} ||= 0;
4147 $itemshash{$key} ++; # += $item->{quantity};
4149 @items = sort { $a->{description} cmp $b->{description} }
4150 map { my $i = thaw($_);
4151 $i->{quantity} = $itemshash{$_};
4153 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4159 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4166 return 0 unless $a->itemdesc cmp $b->itemdesc;
4167 return -1 if $b->itemdesc eq 'Tax';
4168 return 1 if $a->itemdesc eq 'Tax';
4169 return -1 if $b->itemdesc eq 'Other surcharges';
4170 return 1 if $a->itemdesc eq 'Other surcharges';
4171 $a->itemdesc cmp $b->itemdesc;
4176 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4177 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4180 sub _items_cust_bill_pkg {
4182 my $cust_bill_pkg = shift;
4185 my $format = $opt{format} || '';
4186 my $escape_function = $opt{escape_function} || sub { shift };
4187 my $format_function = $opt{format_function} || '';
4188 my $unsquelched = $opt{unsquelched} || '';
4189 my $section = $opt{section}->{description} if $opt{section};
4190 my $summary_page = $opt{summary_page} || '';
4191 my $multilocation = $opt{multilocation} || '';
4192 my $multisection = $opt{multisection} || '';
4193 my $discount_show_always = 0;
4196 my ($s, $r, $u) = ( undef, undef, undef );
4197 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4200 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4201 && $conf->exists('discount-show-always'));
4203 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4204 if ( $_ && !$cust_bill_pkg->hidden ) {
4205 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4206 $_->{amount} =~ s/^\-0\.00$/0.00/;
4207 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4209 unless ( $_->{amount} == 0 && !$discount_show_always );
4214 foreach my $display ( grep { defined($section)
4215 ? $_->section eq $section
4218 #grep { !$_->summary || !$summary_page } # bunk!
4219 grep { !$_->summary || $multisection }
4220 $cust_bill_pkg->cust_bill_pkg_display
4224 my $type = $display->type;
4226 my $desc = $cust_bill_pkg->desc;
4227 $desc = substr($desc, 0, 50). '...'
4228 if $format eq 'latex' && length($desc) > 50;
4230 my %details_opt = ( 'format' => $format,
4231 'escape_function' => $escape_function,
4232 'format_function' => $format_function,
4235 if ( $cust_bill_pkg->pkgnum > 0 ) {
4237 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4239 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4241 my $description = $desc;
4242 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4245 unless ( $cust_pkg->part_pkg->hide_svc_detail
4246 || $cust_bill_pkg->hidden )
4249 push @d, map &{$escape_function}($_),
4250 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4251 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4253 if ( $multilocation ) {
4254 my $loc = $cust_pkg->location_label;
4255 $loc = substr($loc, 0, 50). '...'
4256 if $format eq 'latex' && length($loc) > 50;
4257 push @d, &{$escape_function}($loc);
4262 push @d, $cust_bill_pkg->details(%details_opt)
4263 if $cust_bill_pkg->recur == 0;
4265 if ( $cust_bill_pkg->hidden ) {
4266 $s->{amount} += $cust_bill_pkg->setup;
4267 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4268 push @{ $s->{ext_description} }, @d;
4271 description => $description,
4272 #pkgpart => $part_pkg->pkgpart,
4273 pkgnum => $cust_bill_pkg->pkgnum,
4274 amount => $cust_bill_pkg->setup,
4275 unit_amount => $cust_bill_pkg->unitsetup,
4276 quantity => $cust_bill_pkg->quantity,
4277 ext_description => \@d,
4283 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4284 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4285 ( !$type || $type eq 'R' || $type eq 'U' )
4289 my $is_summary = $display->summary;
4290 my $description = ($is_summary && $type && $type eq 'U')
4291 ? "Usage charges" : $desc;
4293 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4294 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4295 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4300 #at least until cust_bill_pkg has "past" ranges in addition to
4301 #the "future" sdate/edate ones... see #3032
4302 my @dates = ( $self->_date );
4303 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4304 push @dates, $prev->sdate if $prev;
4305 push @dates, undef if !$prev;
4307 unless ( $cust_pkg->part_pkg->hide_svc_detail
4308 || $cust_bill_pkg->itemdesc
4309 || $cust_bill_pkg->hidden
4310 || $is_summary && $type && $type eq 'U' )
4313 push @d, map &{$escape_function}($_),
4314 $cust_pkg->h_labels_short(@dates, 'I')
4315 #$cust_bill_pkg->edate,
4316 #$cust_bill_pkg->sdate)
4317 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4319 if ( $multilocation ) {
4320 my $loc = $cust_pkg->location_label;
4321 $loc = substr($loc, 0, 50). '...'
4322 if $format eq 'latex' && length($loc) > 50;
4323 push @d, &{$escape_function}($loc);
4328 push @d, $cust_bill_pkg->details(%details_opt)
4329 unless ($is_summary || $type && $type eq 'R');
4333 $amount = $cust_bill_pkg->recur;
4334 }elsif($type eq 'R') {
4335 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4336 }elsif($type eq 'U') {
4337 $amount = $cust_bill_pkg->usage;
4340 if ( !$type || $type eq 'R' ) {
4342 if ( $cust_bill_pkg->hidden ) {
4343 $r->{amount} += $amount;
4344 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4345 push @{ $r->{ext_description} }, @d;
4348 description => $description,
4349 #pkgpart => $part_pkg->pkgpart,
4350 pkgnum => $cust_bill_pkg->pkgnum,
4352 unit_amount => $cust_bill_pkg->unitrecur,
4353 quantity => $cust_bill_pkg->quantity,
4354 ext_description => \@d,
4358 } else { # $type eq 'U'
4360 if ( $cust_bill_pkg->hidden ) {
4361 $u->{amount} += $amount;
4362 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4363 push @{ $u->{ext_description} }, @d;
4366 description => $description,
4367 #pkgpart => $part_pkg->pkgpart,
4368 pkgnum => $cust_bill_pkg->pkgnum,
4370 unit_amount => $cust_bill_pkg->unitrecur,
4371 quantity => $cust_bill_pkg->quantity,
4372 ext_description => \@d,
4378 } # recurring or usage with recurring charge
4380 } else { #pkgnum tax or one-shot line item (??)
4382 if ( $cust_bill_pkg->setup != 0 ) {
4384 'description' => $desc,
4385 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4388 if ( $cust_bill_pkg->recur != 0 ) {
4390 'description' => "$desc (".
4391 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4392 time2str($date_format, $cust_bill_pkg->edate). ')',
4393 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4403 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4405 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4406 $_->{amount} =~ s/^\-0\.00$/0.00/;
4407 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4409 unless ( $_->{amount} == 0 && !$discount_show_always );
4417 sub _items_credits {
4418 my( $self, %opt ) = @_;
4419 my $trim_len = $opt{'trim_len'} || 60;
4423 foreach ( $self->cust_credited ) {
4425 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4427 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4428 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4429 $reason = " ($reason) " if $reason;
4432 #'description' => 'Credit ref\#'. $_->crednum.
4433 # " (". time2str("%x",$_->cust_credit->_date) .")".
4435 'description' => 'Credit applied '.
4436 time2str($date_format,$_->cust_credit->_date). $reason,
4437 'amount' => sprintf("%.2f",$_->amount),
4445 sub _items_payments {
4449 #get & print payments
4450 foreach ( $self->cust_bill_pay ) {
4452 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4455 'description' => "Payment received ".
4456 time2str($date_format,$_->cust_pay->_date ),
4457 'amount' => sprintf("%.2f", $_->amount )
4465 =item call_details [ OPTION => VALUE ... ]
4467 Returns an array of CSV strings representing the call details for this invoice
4468 The only option available is the boolean prepend_billed_number
4473 my ($self, %opt) = @_;
4475 my $format_function = sub { shift };
4477 if ($opt{prepend_billed_number}) {
4478 $format_function = sub {
4482 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4487 my @details = map { $_->details( 'format_function' => $format_function,
4488 'escape_function' => sub{ return() },
4492 $self->cust_bill_pkg;
4493 my $header = $details[0];
4494 ( $header, grep { $_ ne $header } @details );
4504 =item process_reprint
4508 sub process_reprint {
4509 process_re_X('print', @_);
4512 =item process_reemail
4516 sub process_reemail {
4517 process_re_X('email', @_);
4525 process_re_X('fax', @_);
4533 process_re_X('ftp', @_);
4540 sub process_respool {
4541 process_re_X('spool', @_);
4544 use Storable qw(thaw);
4548 my( $method, $job ) = ( shift, shift );
4549 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4551 my $param = thaw(decode_base64(shift));
4552 warn Dumper($param) if $DEBUG;
4563 my($method, $job, %param ) = @_;
4565 warn "re_X $method for job $job with param:\n".
4566 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4569 #some false laziness w/search/cust_bill.html
4571 my $orderby = 'ORDER BY cust_bill._date';
4573 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4575 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4577 my @cust_bill = qsearch( {
4578 #'select' => "cust_bill.*",
4579 'table' => 'cust_bill',
4580 'addl_from' => $addl_from,
4582 'extra_sql' => $extra_sql,
4583 'order_by' => $orderby,
4587 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4589 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4592 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4593 foreach my $cust_bill ( @cust_bill ) {
4594 $cust_bill->$method();
4596 if ( $job ) { #progressbar foo
4598 if ( time - $min_sec > $last ) {
4599 my $error = $job->update_statustext(
4600 int( 100 * $num / scalar(@cust_bill) )
4602 die $error if $error;
4613 =head1 CLASS METHODS
4619 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4624 my ($class, $start, $end) = @_;
4626 $class->paid_sql($start, $end). ' - '.
4627 $class->credited_sql($start, $end);
4632 Returns an SQL fragment to retreive the net amount (charged minus credited).
4637 my ($class, $start, $end) = @_;
4638 'charged - '. $class->credited_sql($start, $end);
4643 Returns an SQL fragment to retreive the amount paid against this invoice.
4648 my ($class, $start, $end) = @_;
4649 $start &&= "AND cust_bill_pay._date <= $start";
4650 $end &&= "AND cust_bill_pay._date > $end";
4651 $start = '' unless defined($start);
4652 $end = '' unless defined($end);
4653 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4654 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4659 Returns an SQL fragment to retreive the amount credited against this invoice.
4664 my ($class, $start, $end) = @_;
4665 $start &&= "AND cust_credit_bill._date <= $start";
4666 $end &&= "AND cust_credit_bill._date > $end";
4667 $start = '' unless defined($start);
4668 $end = '' unless defined($end);
4669 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4670 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4675 Returns an SQL fragment to retrieve the due date of an invoice.
4676 Currently only supported on PostgreSQL.
4684 cust_bill.invoice_terms,
4685 cust_main.invoice_terms,
4686 \''.($conf->config('invoice_default_terms') || '').'\'
4687 ), E\'Net (\\\\d+)\'
4689 ) * 86400 + cust_bill._date'
4692 =item search_sql_where HASHREF
4694 Class method which returns an SQL WHERE fragment to search for parameters
4695 specified in HASHREF. Valid parameters are
4701 List reference of start date, end date, as UNIX timestamps.
4711 List reference of charged limits (exclusive).
4715 List reference of charged limits (exclusive).
4719 flag, return open invoices only
4723 flag, return net invoices only
4727 =item newest_percust
4731 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4735 sub search_sql_where {
4736 my($class, $param) = @_;
4738 warn "$me search_sql_where called with params: \n".
4739 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4745 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4746 push @search, "cust_main.agentnum = $1";
4750 if ( $param->{_date} ) {
4751 my($beginning, $ending) = @{$param->{_date}};
4753 push @search, "cust_bill._date >= $beginning",
4754 "cust_bill._date < $ending";
4758 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4759 push @search, "cust_bill.invnum >= $1";
4761 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4762 push @search, "cust_bill.invnum <= $1";
4766 if ( $param->{charged} ) {
4767 my @charged = ref($param->{charged})
4768 ? @{ $param->{charged} }
4769 : ($param->{charged});
4771 push @search, map { s/^charged/cust_bill.charged/; $_; }
4775 my $owed_sql = FS::cust_bill->owed_sql;
4778 if ( $param->{owed} ) {
4779 my @owed = ref($param->{owed})
4780 ? @{ $param->{owed} }
4782 push @search, map { s/^owed/$owed_sql/; $_; }
4787 push @search, "0 != $owed_sql"
4788 if $param->{'open'};
4789 push @search, '0 != '. FS::cust_bill->net_sql
4793 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4794 if $param->{'days'};
4797 if ( $param->{'newest_percust'} ) {
4799 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4800 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4802 my @newest_where = map { my $x = $_;
4803 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4806 grep ! /^cust_main./, @search;
4807 my $newest_where = scalar(@newest_where)
4808 ? ' AND '. join(' AND ', @newest_where)
4812 push @search, "cust_bill._date = (
4813 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4814 WHERE newest_cust_bill.custnum = cust_bill.custnum
4820 #agent virtualization
4821 my $curuser = $FS::CurrentUser::CurrentUser;
4822 if ( $curuser->username eq 'fs_queue'
4823 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4825 my $newuser = qsearchs('access_user', {
4826 'username' => $username,
4830 $curuser = $newuser;
4832 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4835 push @search, $curuser->agentnums_sql;
4837 join(' AND ', @search );
4849 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4850 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base