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 #create the template
2190 my $template = $params{template} ? $params{template} : $self->_agent_template;
2191 my $templatefile = "invoice_$format";
2192 $templatefile .= "_$template"
2193 if length($template);
2194 my @invoice_template = map "$_\n", $conf->config($templatefile)
2195 or die "cannot load config data $templatefile";
2198 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2199 #change this to a die when the old code is removed
2200 warn "old-style invoice template $templatefile; ".
2201 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2202 $old_latex = 'true';
2203 @invoice_template = _translate_old_latex_format(@invoice_template);
2206 my $text_template = new Text::Template(
2208 SOURCE => \@invoice_template,
2209 DELIMITERS => $delimiters{$format},
2212 $text_template->compile()
2213 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2216 # additional substitution could possibly cause breakage in existing templates
2217 my %convert_maps = (
2219 'notes' => sub { map "$_", @_ },
2220 'footer' => sub { map "$_", @_ },
2221 'smallfooter' => sub { map "$_", @_ },
2222 'returnaddress' => sub { map "$_", @_ },
2223 'coupon' => sub { map "$_", @_ },
2224 'summary' => sub { map "$_", @_ },
2230 s/%%(.*)$/<!-- $1 -->/g;
2231 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2232 s/\\begin\{enumerate\}/<ol>/g;
2234 s/\\end\{enumerate\}/<\/ol>/g;
2235 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2244 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2246 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2251 s/\\\\\*?\s*$/<BR>/;
2252 s/\\hyphenation\{[\w\s\-]+}//;
2257 'coupon' => sub { "" },
2258 'summary' => sub { "" },
2265 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2266 s/\\begin\{enumerate\}//g;
2268 s/\\end\{enumerate\}//g;
2269 s/\\textbf\{(.*)\}/$1/g;
2276 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2278 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2283 s/\\\\\*?\s*$/\n/; # dubious
2284 s/\\hyphenation\{[\w\s\-]+}//;
2288 'coupon' => sub { "" },
2289 'summary' => sub { "" },
2294 # hashes for differing output formats
2295 my %nbsps = ( 'latex' => '~',
2296 'html' => '', # '&nbps;' would be nice
2297 'template' => '', # not used
2299 my $nbsp = $nbsps{$format};
2301 my %escape_functions = ( 'latex' => \&_latex_escape,
2302 'html' => \&_html_escape_nbsp,#\&encode_entities,
2303 'template' => sub { shift },
2305 my $escape_function = $escape_functions{$format};
2306 my $escape_function_nonbsp = ($format eq 'html')
2307 ? \&_html_escape : $escape_function;
2309 my %date_formats = ( 'latex' => $date_format_long,
2310 'html' => $date_format_long,
2313 $date_formats{'html'} =~ s/ / /g;
2315 my $date_format = $date_formats{$format};
2317 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2319 'html' => sub { return '<b>'. shift(). '</b>'
2321 'template' => sub { shift },
2323 my $embolden_function = $embolden_functions{$format};
2326 # generate template variables
2329 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2333 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2339 $returnaddress = join("\n",
2340 $conf->config_orbase("invoice_${format}returnaddress", $template)
2343 } elsif ( grep /\S/,
2344 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2346 my $convert_map = $convert_maps{$format}{'returnaddress'};
2349 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2354 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2356 my $convert_map = $convert_maps{$format}{'returnaddress'};
2357 $returnaddress = join( "\n", &$convert_map(
2358 map { s/( {2,})/'~' x length($1)/eg;
2362 ( $conf->config('company_name', $self->cust_main->agentnum),
2363 $conf->config('company_address', $self->cust_main->agentnum),
2370 my $warning = "Couldn't find a return address; ".
2371 "do you need to set the company_address configuration value?";
2373 $returnaddress = $nbsp;
2374 #$returnaddress = $warning;
2378 my $agentnum = $self->cust_main->agentnum;
2380 my %invoice_data = (
2383 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2384 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2385 'returnaddress' => $returnaddress,
2386 'agent' => &$escape_function($cust_main->agent->agent),
2389 'invnum' => $self->invnum,
2390 'date' => time2str($date_format, $self->_date),
2391 'today' => time2str($date_format_long, $today),
2392 'terms' => $self->terms,
2393 'template' => $template, #params{'template'},
2394 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2395 'current_charges' => sprintf("%.2f", $self->charged),
2396 'duedate' => $self->due_date2str($rdate_format), #date_format?
2399 'custnum' => $cust_main->display_custnum,
2400 'agent_custid' => &$escape_function($cust_main->agent_custid),
2401 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2402 payname company address1 address2 city state zip fax
2406 'ship_enable' => $conf->exists('invoice-ship_address'),
2407 'unitprices' => $conf->exists('invoice-unitprice'),
2408 'smallernotes' => $conf->exists('invoice-smallernotes'),
2409 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2410 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2412 #layout info -- would be fancy to calc some of this and bury the template
2414 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2415 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2416 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2417 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2418 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2419 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2420 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2421 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2422 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2423 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2425 # better hang on to conf_dir for a while (for old templates)
2426 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2428 #these are only used when doing paged plaintext
2434 $invoice_data{finance_section} = '';
2435 if ( $conf->config('finance_pkgclass') ) {
2437 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2438 $invoice_data{finance_section} = $pkg_class->categoryname;
2440 $invoice_data{finance_amount} = '0.00';
2441 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2443 my $countrydefault = $conf->config('countrydefault') || 'US';
2444 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2445 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2446 my $method = $prefix.$_;
2447 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2449 $invoice_data{'ship_country'} = ''
2450 if ( $invoice_data{'ship_country'} eq $countrydefault );
2452 $invoice_data{'cid'} = $params{'cid'}
2455 if ( $cust_main->country eq $countrydefault ) {
2456 $invoice_data{'country'} = '';
2458 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2462 $invoice_data{'address'} = \@address;
2464 $cust_main->payname.
2465 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2466 ? " (P.O. #". $cust_main->payinfo. ")"
2470 push @address, $cust_main->company
2471 if $cust_main->company;
2472 push @address, $cust_main->address1;
2473 push @address, $cust_main->address2
2474 if $cust_main->address2;
2476 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2477 push @address, $invoice_data{'country'}
2478 if $invoice_data{'country'};
2480 while (scalar(@address) < 5);
2482 $invoice_data{'logo_file'} = $params{'logo_file'}
2483 if $params{'logo_file'};
2485 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2486 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2487 #my $balance_due = $self->owed + $pr_total - $cr_total;
2488 my $balance_due = $self->owed + $pr_total;
2489 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2490 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2491 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2492 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2494 my $summarypage = '';
2495 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2498 $invoice_data{'summarypage'} = $summarypage;
2500 #do variable substitution in notes, footer, smallfooter
2501 foreach my $include (qw( notes footer smallfooter coupon )) {
2503 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2506 if ( $conf->exists($inc_file, $agentnum)
2507 && length( $conf->config($inc_file, $agentnum) ) ) {
2509 @inc_src = $conf->config($inc_file, $agentnum);
2513 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2515 my $convert_map = $convert_maps{$format}{$include};
2517 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2518 s/--\@\]/$delimiters{$format}[1]/g;
2521 &$convert_map( $conf->config($inc_file, $agentnum) );
2525 my $inc_tt = new Text::Template (
2527 SOURCE => [ map "$_\n", @inc_src ],
2528 DELIMITERS => $delimiters{$format},
2529 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2531 unless ( $inc_tt->compile() ) {
2532 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2533 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2537 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2539 $invoice_data{$include} =~ s/\n+$//
2540 if ($format eq 'latex');
2543 $invoice_data{'po_line'} =
2544 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2545 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2548 my %money_chars = ( 'latex' => '',
2549 'html' => $conf->config('money_char') || '$',
2552 my $money_char = $money_chars{$format};
2554 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2555 'html' => $conf->config('money_char') || '$',
2558 my $other_money_char = $other_money_chars{$format};
2559 $invoice_data{'dollar'} = $other_money_char;
2561 my @detail_items = ();
2562 my @total_items = ();
2566 $invoice_data{'detail_items'} = \@detail_items;
2567 $invoice_data{'total_items'} = \@total_items;
2568 $invoice_data{'buf'} = \@buf;
2569 $invoice_data{'sections'} = \@sections;
2571 my $previous_section = { 'description' => 'Previous Charges',
2572 'subtotal' => $other_money_char.
2573 sprintf('%.2f', $pr_total),
2574 'summarized' => $summarypage ? 'Y' : '',
2576 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2577 join(' / ', map { $cust_main->balance_date_range(@$_) }
2578 $self->_prior_month30s
2580 if $conf->exists('invoice_include_aging');
2583 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2584 'subtotal' => $taxtotal, # adjusted below
2585 'summarized' => $summarypage ? 'Y' : '',
2587 my $tax_weight = _pkg_category($tax_section->{description})
2588 ? _pkg_category($tax_section->{description})->weight
2590 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2591 $tax_section->{'sort_weight'} = $tax_weight;
2594 my $adjusttotal = 0;
2595 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2596 'subtotal' => 0, # adjusted below
2597 'summarized' => $summarypage ? 'Y' : '',
2599 my $adjust_weight = _pkg_category($adjust_section->{description})
2600 ? _pkg_category($adjust_section->{description})->weight
2602 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2603 $adjust_section->{'sort_weight'} = $adjust_weight;
2605 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2606 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2607 $invoice_data{'multisection'} = $multisection;
2608 my $late_sections = [];
2609 my $extra_sections = [];
2610 my $extra_lines = ();
2611 if ( $multisection ) {
2612 ($extra_sections, $extra_lines) =
2613 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2614 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2616 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2618 push @detail_items, @$extra_lines if $extra_lines;
2620 $self->_items_sections( $late_sections, # this could stand a refactor
2622 $escape_function_nonbsp,
2626 if ($conf->exists('svc_phone_sections')) {
2627 my ($phone_sections, $phone_lines) =
2628 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2629 push @{$late_sections}, @$phone_sections;
2630 push @detail_items, @$phone_lines;
2633 push @sections, { 'description' => '', 'subtotal' => '' };
2636 unless ( $conf->exists('disable_previous_balance')
2637 || $conf->exists('previous_balance-summary_only')
2641 foreach my $line_item ( $self->_items_previous ) {
2644 ext_description => [],
2646 $detail->{'ref'} = $line_item->{'pkgnum'};
2647 $detail->{'quantity'} = 1;
2648 $detail->{'section'} = $previous_section;
2649 $detail->{'description'} = &$escape_function($line_item->{'description'});
2650 if ( exists $line_item->{'ext_description'} ) {
2651 @{$detail->{'ext_description'}} = map {
2652 &$escape_function($_);
2653 } @{$line_item->{'ext_description'}};
2655 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2656 $line_item->{'amount'};
2657 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2659 push @detail_items, $detail;
2660 push @buf, [ $detail->{'description'},
2661 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2667 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2668 push @buf, ['','-----------'];
2669 push @buf, [ 'Total Previous Balance',
2670 $money_char. sprintf("%10.2f", $pr_total) ];
2674 if ( $conf->exists('svc_phone-did-summary') ) {
2675 my ($didsummary,$minutes) = $self->_did_summary;
2676 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2678 { 'description' => $didsummary_desc,
2679 'ext_description' => [ $didsummary, $minutes ],
2684 foreach my $section (@sections, @$late_sections) {
2686 # begin some normalization
2687 $section->{'subtotal'} = $section->{'amount'}
2689 && !exists($section->{subtotal})
2690 && exists($section->{amount});
2692 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2693 if ( $invoice_data{finance_section} &&
2694 $section->{'description'} eq $invoice_data{finance_section} );
2696 $section->{'subtotal'} = $other_money_char.
2697 sprintf('%.2f', $section->{'subtotal'})
2700 # continue some normalization
2701 $section->{'amount'} = $section->{'subtotal'}
2705 if ( $section->{'description'} ) {
2706 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2711 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2713 $options{'section'} = $section if $multisection;
2714 $options{'format'} = $format;
2715 $options{'escape_function'} = $escape_function;
2716 $options{'format_function'} = sub { () } unless $unsquelched;
2717 $options{'unsquelched'} = $unsquelched;
2718 $options{'summary_page'} = $summarypage;
2719 $options{'skip_usage'} =
2720 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2721 $options{'multilocation'} = $multilocation;
2722 $options{'multisection'} = $multisection;
2724 foreach my $line_item ( $self->_items_pkg(%options) ) {
2726 ext_description => [],
2728 $detail->{'ref'} = $line_item->{'pkgnum'};
2729 $detail->{'quantity'} = $line_item->{'quantity'};
2730 $detail->{'section'} = $section;
2731 $detail->{'description'} = &$escape_function($line_item->{'description'});
2732 if ( exists $line_item->{'ext_description'} ) {
2733 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2735 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2736 $line_item->{'amount'};
2737 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2738 $line_item->{'unit_amount'};
2739 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2741 push @detail_items, $detail;
2742 push @buf, ( [ $detail->{'description'},
2743 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2745 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2749 if ( $section->{'description'} ) {
2750 push @buf, ( ['','-----------'],
2751 [ $section->{'description'}. ' sub-total',
2752 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2761 $invoice_data{current_less_finance} =
2762 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2764 if ( $multisection && !$conf->exists('disable_previous_balance')
2765 || $conf->exists('previous_balance-summary_only') )
2767 unshift @sections, $previous_section if $pr_total;
2770 foreach my $tax ( $self->_items_tax ) {
2772 $taxtotal += $tax->{'amount'};
2774 my $description = &$escape_function( $tax->{'description'} );
2775 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2777 if ( $multisection ) {
2779 my $money = $old_latex ? '' : $money_char;
2780 push @detail_items, {
2781 ext_description => [],
2784 description => $description,
2785 amount => $money. $amount,
2787 section => $tax_section,
2792 push @total_items, {
2793 'total_item' => $description,
2794 'total_amount' => $other_money_char. $amount,
2799 push @buf,[ $description,
2800 $money_char. $amount,
2807 $total->{'total_item'} = 'Sub-total';
2808 $total->{'total_amount'} =
2809 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2811 if ( $multisection ) {
2812 $tax_section->{'subtotal'} = $other_money_char.
2813 sprintf('%.2f', $taxtotal);
2814 $tax_section->{'pretotal'} = 'New charges sub-total '.
2815 $total->{'total_amount'};
2816 push @sections, $tax_section if $taxtotal;
2818 unshift @total_items, $total;
2821 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2823 push @buf,['','-----------'];
2824 push @buf,[( $conf->exists('disable_previous_balance')
2826 : 'Total New Charges'
2828 $money_char. sprintf("%10.2f",$self->charged) ];
2834 $item = $conf->config('previous_balance-exclude_from_total')
2835 || 'Total New Charges'
2836 if $conf->exists('previous_balance-exclude_from_total');
2837 my $amount = $self->charged +
2838 ( $conf->exists('disable_previous_balance') ||
2839 $conf->exists('previous_balance-exclude_from_total')
2843 $total->{'total_item'} = &$embolden_function($item);
2844 $total->{'total_amount'} =
2845 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2846 if ( $multisection ) {
2847 if ( $adjust_section->{'sort_weight'} ) {
2848 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2849 sprintf("%.2f", ($self->billing_balance || 0) );
2851 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2852 sprintf('%.2f', $self->charged );
2855 push @total_items, $total;
2857 push @buf,['','-----------'];
2860 sprintf( '%10.2f', $amount )
2865 unless ( $conf->exists('disable_previous_balance') ) {
2866 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2869 my $credittotal = 0;
2870 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2873 $total->{'total_item'} = &$escape_function($credit->{'description'});
2874 $credittotal += $credit->{'amount'};
2875 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2876 $adjusttotal += $credit->{'amount'};
2877 if ( $multisection ) {
2878 my $money = $old_latex ? '' : $money_char;
2879 push @detail_items, {
2880 ext_description => [],
2883 description => &$escape_function($credit->{'description'}),
2884 amount => $money. $credit->{'amount'},
2886 section => $adjust_section,
2889 push @total_items, $total;
2893 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2896 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2897 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2901 my $paymenttotal = 0;
2902 foreach my $payment ( $self->_items_payments ) {
2904 $total->{'total_item'} = &$escape_function($payment->{'description'});
2905 $paymenttotal += $payment->{'amount'};
2906 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2907 $adjusttotal += $payment->{'amount'};
2908 if ( $multisection ) {
2909 my $money = $old_latex ? '' : $money_char;
2910 push @detail_items, {
2911 ext_description => [],
2914 description => &$escape_function($payment->{'description'}),
2915 amount => $money. $payment->{'amount'},
2917 section => $adjust_section,
2920 push @total_items, $total;
2922 push @buf, [ $payment->{'description'},
2923 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2926 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2928 if ( $multisection ) {
2929 $adjust_section->{'subtotal'} = $other_money_char.
2930 sprintf('%.2f', $adjusttotal);
2931 push @sections, $adjust_section
2932 unless $adjust_section->{sort_weight};
2937 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2938 $total->{'total_amount'} =
2939 &$embolden_function(
2940 $other_money_char. sprintf('%.2f', $summarypage
2942 $self->billing_balance
2943 : $self->owed + $pr_total
2946 if ( $multisection && !$adjust_section->{sort_weight} ) {
2947 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2948 $total->{'total_amount'};
2950 push @total_items, $total;
2952 push @buf,['','-----------'];
2953 push @buf,[$self->balance_due_msg, $money_char.
2954 sprintf("%10.2f", $balance_due ) ];
2958 if ( $multisection ) {
2959 if ($conf->exists('svc_phone_sections')) {
2961 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2962 $total->{'total_amount'} =
2963 &$embolden_function(
2964 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2966 my $last_section = pop @sections;
2967 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2968 $total->{'total_amount'};
2969 push @sections, $last_section;
2971 push @sections, @$late_sections
2975 my @includelist = ();
2976 push @includelist, 'summary' if $summarypage;
2977 foreach my $include ( @includelist ) {
2979 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2982 if ( length( $conf->config($inc_file, $agentnum) ) ) {
2984 @inc_src = $conf->config($inc_file, $agentnum);
2988 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2990 my $convert_map = $convert_maps{$format}{$include};
2992 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2993 s/--\@\]/$delimiters{$format}[1]/g;
2996 &$convert_map( $conf->config($inc_file, $agentnum) );
3000 my $inc_tt = new Text::Template (
3002 SOURCE => [ map "$_\n", @inc_src ],
3003 DELIMITERS => $delimiters{$format},
3004 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3006 unless ( $inc_tt->compile() ) {
3007 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3008 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3012 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3014 $invoice_data{$include} =~ s/\n+$//
3015 if ($format eq 'latex');
3020 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3021 /invoice_lines\((\d*)\)/;
3022 $invoice_lines += $1 || scalar(@buf);
3025 die "no invoice_lines() functions in template?"
3026 if ( $format eq 'template' && !$wasfunc );
3028 if ($format eq 'template') {
3030 if ( $invoice_lines ) {
3031 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3032 $invoice_data{'total_pages'}++
3033 if scalar(@buf) % $invoice_lines;
3036 #setup subroutine for the template
3037 sub FS::cust_bill::_template::invoice_lines {
3038 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3040 scalar(@FS::cust_bill::_template::buf)
3041 ? shift @FS::cust_bill::_template::buf
3050 push @collect, split("\n",
3051 $text_template->fill_in( HASH => \%invoice_data,
3052 PACKAGE => 'FS::cust_bill::_template'
3055 $FS::cust_bill::_template::page++;
3057 map "$_\n", @collect;
3059 warn "filling in template for invoice ". $self->invnum. "\n"
3061 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3064 $text_template->fill_in(HASH => \%invoice_data);
3068 # helper routine for generating date ranges
3069 sub _prior_month30s {
3072 [ 1, 2592000 ], # 0-30 days ago
3073 [ 2592000, 5184000 ], # 30-60 days ago
3074 [ 5184000, 7776000 ], # 60-90 days ago
3075 [ 7776000, 0 ], # 90+ days ago
3078 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3079 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3084 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3086 Returns an postscript invoice, as a scalar.
3088 Options can be passed as a hashref (recommended) or as a list of time, template
3089 and then any key/value pairs for any other options.
3091 I<time> an optional value used to control the printing of overdue messages. The
3092 default is now. It isn't the date of the invoice; that's the `_date' field.
3093 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3094 L<Time::Local> and L<Date::Parse> for conversion functions.
3096 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3103 my ($file, $lfile) = $self->print_latex(@_);
3104 my $ps = generate_ps($file);
3110 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3112 Returns an PDF invoice, as a scalar.
3114 Options can be passed as a hashref (recommended) or as a list of time, template
3115 and then any key/value pairs for any other options.
3117 I<time> an optional value used to control the printing of overdue messages. The
3118 default is now. It isn't the date of the invoice; that's the `_date' field.
3119 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3120 L<Time::Local> and L<Date::Parse> for conversion functions.
3122 I<template>, if specified, is the name of a suffix for alternate invoices.
3124 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3131 my ($file, $lfile) = $self->print_latex(@_);
3132 my $pdf = generate_pdf($file);
3138 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3140 Returns an HTML invoice, as a scalar.
3142 I<time> an optional value used to control the printing of overdue messages. The
3143 default is now. It isn't the date of the invoice; that's the `_date' field.
3144 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3145 L<Time::Local> and L<Date::Parse> for conversion functions.
3147 I<template>, if specified, is the name of a suffix for alternate invoices.
3149 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3151 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3152 when emailing the invoice as part of a multipart/related MIME email.
3160 %params = %{ shift() };
3162 $params{'time'} = shift;
3163 $params{'template'} = shift;
3164 $params{'cid'} = shift;
3167 $params{'format'} = 'html';
3169 $self->print_generic( %params );
3172 # quick subroutine for print_latex
3174 # There are ten characters that LaTeX treats as special characters, which
3175 # means that they do not simply typeset themselves:
3176 # # $ % & ~ _ ^ \ { }
3178 # TeX ignores blanks following an escaped character; if you want a blank (as
3179 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3183 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3184 $value =~ s/([<>])/\$$1\$/g;
3190 encode_entities($value);
3194 sub _html_escape_nbsp {
3195 my $value = _html_escape(shift);
3196 $value =~ s/ +/ /g;
3200 #utility methods for print_*
3202 sub _translate_old_latex_format {
3203 warn "_translate_old_latex_format called\n"
3210 if ( $line =~ /^%%Detail\s*$/ ) {
3212 push @template, q![@--!,
3213 q! foreach my $_tr_line (@detail_items) {!,
3214 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3215 q! $_tr_line->{'description'} .= !,
3216 q! "\\tabularnewline\n~~".!,
3217 q! join( "\\tabularnewline\n~~",!,
3218 q! @{$_tr_line->{'ext_description'}}!,
3222 while ( ( my $line_item_line = shift )
3223 !~ /^%%EndDetail\s*$/ ) {
3224 $line_item_line =~ s/'/\\'/g; # nice LTS
3225 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3226 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3227 push @template, " \$OUT .= '$line_item_line';";
3230 push @template, '}',
3233 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3235 push @template, '[@--',
3236 ' foreach my $_tr_line (@total_items) {';
3238 while ( ( my $total_item_line = shift )
3239 !~ /^%%EndTotalDetails\s*$/ ) {
3240 $total_item_line =~ s/'/\\'/g; # nice LTS
3241 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3242 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3243 push @template, " \$OUT .= '$total_item_line';";
3246 push @template, '}',
3250 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3251 push @template, $line;
3257 warn "$_\n" foreach @template;
3266 #check for an invoice-specific override
3267 return $self->invoice_terms if $self->invoice_terms;
3269 #check for a customer- specific override
3270 my $cust_main = $self->cust_main;
3271 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3273 #use configured default
3274 $conf->config('invoice_default_terms') || '';
3280 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3281 $duedate = $self->_date() + ( $1 * 86400 );
3288 $self->due_date ? time2str(shift, $self->due_date) : '';
3291 sub balance_due_msg {
3293 my $msg = 'Balance Due';
3294 return $msg unless $self->terms;
3295 if ( $self->due_date ) {
3296 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3297 } elsif ( $self->terms ) {
3298 $msg .= ' - '. $self->terms;
3303 sub balance_due_date {
3306 if ( $conf->exists('invoice_default_terms')
3307 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3308 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3313 =item invnum_date_pretty
3315 Returns a string with the invoice number and date, for example:
3316 "Invoice #54 (3/20/2008)"
3320 sub invnum_date_pretty {
3322 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3327 Returns a string with the date, for example: "3/20/2008"
3333 time2str($date_format, $self->_date);
3336 use vars qw(%pkg_category_cache);
3337 sub _items_sections {
3340 my $summarypage = shift;
3342 my $extra_sections = shift;
3346 my %late_subtotal = ();
3349 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3352 my $usage = $cust_bill_pkg->usage;
3354 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3355 next if ( $display->summary && $summarypage );
3357 my $section = $display->section;
3358 my $type = $display->type;
3360 $not_tax{$section} = 1
3361 unless $cust_bill_pkg->pkgnum == 0;
3363 if ( $display->post_total && !$summarypage ) {
3364 if (! $type || $type eq 'S') {
3365 $late_subtotal{$section} += $cust_bill_pkg->setup
3366 if $cust_bill_pkg->setup != 0;
3370 $late_subtotal{$section} += $cust_bill_pkg->recur
3371 if $cust_bill_pkg->recur != 0;
3374 if ($type && $type eq 'R') {
3375 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3376 if $cust_bill_pkg->recur != 0;
3379 if ($type && $type eq 'U') {
3380 $late_subtotal{$section} += $usage
3381 unless scalar(@$extra_sections);
3386 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3388 if (! $type || $type eq 'S') {
3389 $subtotal{$section} += $cust_bill_pkg->setup
3390 if $cust_bill_pkg->setup != 0;
3394 $subtotal{$section} += $cust_bill_pkg->recur
3395 if $cust_bill_pkg->recur != 0;
3398 if ($type && $type eq 'R') {
3399 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3400 if $cust_bill_pkg->recur != 0;
3403 if ($type && $type eq 'U') {
3404 $subtotal{$section} += $usage
3405 unless scalar(@$extra_sections);
3414 %pkg_category_cache = ();
3416 push @$late, map { { 'description' => &{$escape}($_),
3417 'subtotal' => $late_subtotal{$_},
3419 'sort_weight' => ( _pkg_category($_)
3420 ? _pkg_category($_)->weight
3423 ((_pkg_category($_) && _pkg_category($_)->condense)
3424 ? $self->_condense_section($format)
3428 sort _sectionsort keys %late_subtotal;
3431 if ( $summarypage ) {
3432 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3433 map { $_->categoryname } qsearch('pkg_category', {});
3434 push @sections, '' if exists($subtotal{''});
3436 @sections = keys %subtotal;
3439 my @early = map { { 'description' => &{$escape}($_),
3440 'subtotal' => $subtotal{$_},
3441 'summarized' => $not_tax{$_} ? '' : 'Y',
3442 'tax_section' => $not_tax{$_} ? '' : 'Y',
3443 'sort_weight' => ( _pkg_category($_)
3444 ? _pkg_category($_)->weight
3447 ((_pkg_category($_) && _pkg_category($_)->condense)
3448 ? $self->_condense_section($format)
3453 push @early, @$extra_sections if $extra_sections;
3455 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3459 #helper subs for above
3462 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3466 my $categoryname = shift;
3467 $pkg_category_cache{$categoryname} ||=
3468 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3471 my %condensed_format = (
3472 'label' => [ qw( Description Qty Amount ) ],
3474 sub { shift->{description} },
3475 sub { shift->{quantity} },
3476 sub { my($href, %opt) = @_;
3477 ($opt{dollar} || ''). $href->{amount};
3480 'align' => [ qw( l r r ) ],
3481 'span' => [ qw( 5 1 1 ) ], # unitprices?
3482 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3485 sub _condense_section {
3486 my ( $self, $format ) = ( shift, shift );
3488 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3489 qw( description_generator
3492 total_line_generator
3497 sub _condensed_generator_defaults {
3498 my ( $self, $format ) = ( shift, shift );
3499 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3508 sub _condensed_header_generator {
3509 my ( $self, $format ) = ( shift, shift );
3511 my ( $f, $prefix, $suffix, $separator, $column ) =
3512 _condensed_generator_defaults($format);
3514 if ($format eq 'latex') {
3515 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3516 $suffix = "\\\\\n\\hline";
3519 sub { my ($d,$a,$s,$w) = @_;
3520 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3522 } elsif ( $format eq 'html' ) {
3523 $prefix = '<th></th>';
3527 sub { my ($d,$a,$s,$w) = @_;
3528 return qq!<th align="$html_align{$a}">$d</th>!;
3536 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3538 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3541 $prefix. join($separator, @result). $suffix;
3546 sub _condensed_description_generator {
3547 my ( $self, $format ) = ( shift, shift );
3549 my ( $f, $prefix, $suffix, $separator, $column ) =
3550 _condensed_generator_defaults($format);
3552 my $money_char = '$';
3553 if ($format eq 'latex') {
3554 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3556 $separator = " & \n";
3558 sub { my ($d,$a,$s,$w) = @_;
3559 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3561 $money_char = '\\dollar';
3562 }elsif ( $format eq 'html' ) {
3563 $prefix = '"><td align="center"></td>';
3567 sub { my ($d,$a,$s,$w) = @_;
3568 return qq!<td align="$html_align{$a}">$d</td>!;
3570 #$money_char = $conf->config('money_char') || '$';
3571 $money_char = ''; # this is madness
3579 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3581 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3583 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3584 map { $f->{$_}->[$i] } qw(align span width)
3588 $prefix. join( $separator, @result ). $suffix;
3593 sub _condensed_total_generator {
3594 my ( $self, $format ) = ( shift, shift );
3596 my ( $f, $prefix, $suffix, $separator, $column ) =
3597 _condensed_generator_defaults($format);
3600 if ($format eq 'latex') {
3603 $separator = " & \n";
3605 sub { my ($d,$a,$s,$w) = @_;
3606 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3608 }elsif ( $format eq 'html' ) {
3612 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3614 sub { my ($d,$a,$s,$w) = @_;
3615 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3624 # my $r = &{$f->{fields}->[$i]}(@args);
3625 # $r .= ' Total' unless $i;
3627 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3629 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3630 map { $f->{$_}->[$i] } qw(align span width)
3634 $prefix. join( $separator, @result ). $suffix;
3639 =item total_line_generator FORMAT
3641 Returns a coderef used for generation of invoice total line items for this
3642 usage_class. FORMAT is either html or latex
3646 # should not be used: will have issues with hash element names (description vs
3647 # total_item and amount vs total_amount -- another array of functions?
3649 sub _condensed_total_line_generator {
3650 my ( $self, $format ) = ( shift, shift );
3652 my ( $f, $prefix, $suffix, $separator, $column ) =
3653 _condensed_generator_defaults($format);
3656 if ($format eq 'latex') {
3659 $separator = " & \n";
3661 sub { my ($d,$a,$s,$w) = @_;
3662 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3664 }elsif ( $format eq 'html' ) {
3668 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3670 sub { my ($d,$a,$s,$w) = @_;
3671 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3680 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3682 &{$column}( &{$f->{fields}->[$i]}(@args),
3683 map { $f->{$_}->[$i] } qw(align span width)
3687 $prefix. join( $separator, @result ). $suffix;
3692 #sub _items_extra_usage_sections {
3694 # my $escape = shift;
3696 # my %sections = ();
3698 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3699 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3701 # next unless $cust_bill_pkg->pkgnum > 0;
3703 # foreach my $section ( keys %usage_class ) {
3705 # my $usage = $cust_bill_pkg->usage($section);
3707 # next unless $usage && $usage > 0;
3709 # $sections{$section} ||= 0;
3710 # $sections{$section} += $usage;
3716 # map { { 'description' => &{$escape}($_),
3717 # 'subtotal' => $sections{$_},
3718 # 'summarized' => '',
3719 # 'tax_section' => '',
3722 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3726 sub _items_extra_usage_sections {
3735 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3736 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3737 next unless $cust_bill_pkg->pkgnum > 0;
3739 foreach my $classnum ( keys %usage_class ) {
3740 my $section = $usage_class{$classnum}->classname;
3741 $classnums{$section} = $classnum;
3743 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3744 my $amount = $detail->amount;
3745 next unless $amount && $amount > 0;
3747 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3748 $sections{$section}{amount} += $amount; #subtotal
3749 $sections{$section}{calls}++;
3750 $sections{$section}{duration} += $detail->duration;
3752 my $desc = $detail->regionname;
3753 my $description = $desc;
3754 $description = substr($desc, 0, 50). '...'
3755 if $format eq 'latex' && length($desc) > 50;
3757 $lines{$section}{$desc} ||= {
3758 description => &{$escape}($description),
3759 #pkgpart => $part_pkg->pkgpart,
3760 pkgnum => $cust_bill_pkg->pkgnum,
3765 #unit_amount => $cust_bill_pkg->unitrecur,
3766 quantity => $cust_bill_pkg->quantity,
3767 product_code => 'N/A',
3768 ext_description => [],
3771 $lines{$section}{$desc}{amount} += $amount;
3772 $lines{$section}{$desc}{calls}++;
3773 $lines{$section}{$desc}{duration} += $detail->duration;
3779 my %sectionmap = ();
3780 foreach (keys %sections) {
3781 my $usage_class = $usage_class{$classnums{$_}};
3782 $sectionmap{$_} = { 'description' => &{$escape}($_),
3783 'amount' => $sections{$_}{amount}, #subtotal
3784 'calls' => $sections{$_}{calls},
3785 'duration' => $sections{$_}{duration},
3787 'tax_section' => '',
3788 'sort_weight' => $usage_class->weight,
3789 ( $usage_class->format
3790 ? ( map { $_ => $usage_class->$_($format) }
3791 qw( description_generator header_generator total_generator total_line_generator )
3798 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3802 foreach my $section ( keys %lines ) {
3803 foreach my $line ( keys %{$lines{$section}} ) {
3804 my $l = $lines{$section}{$line};
3805 $l->{section} = $sectionmap{$section};
3806 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3807 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3812 return(\@sections, \@lines);
3818 my $end = $self->_date;
3819 my $start = $end - 2592000; # 30 days
3820 my $cust_main = $self->cust_main;
3821 my @pkgs = $cust_main->all_pkgs;
3822 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3825 foreach my $pkg ( @pkgs ) {
3826 my @h_cust_svc = $pkg->h_cust_svc($end);
3827 foreach my $h_cust_svc ( @h_cust_svc ) {
3828 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3829 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3831 my $inserted = $h_cust_svc->date_inserted;
3832 my $deleted = $h_cust_svc->date_deleted;
3833 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3835 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3837 # DID either activated or ported in; cannot be both for same DID simultaneously
3838 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3839 && (!$phone_inserted->lnp_status
3840 || $phone_inserted->lnp_status eq ''
3841 || $phone_inserted->lnp_status eq 'native')) {
3844 else { # this one not so clean, should probably move to (h_)svc_phone
3845 my $phone_portedin = qsearchs( 'h_svc_phone',
3846 { 'svcnum' => $h_cust_svc->svcnum,
3847 'lnp_status' => 'portedin' },
3848 FS::h_svc_phone->sql_h_searchs($end),
3850 $num_portedin++ if $phone_portedin;
3853 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3854 if($deleted >= $start && $deleted <= $end && $phone_deleted
3855 && (!$phone_deleted->lnp_status
3856 || $phone_deleted->lnp_status ne 'portingout')) {
3859 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3860 && $phone_deleted->lnp_status
3861 && $phone_deleted->lnp_status eq 'portingout') {
3865 # increment usage minutes
3866 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3867 foreach my $cdr ( @cdrs ) {
3868 $minutes += $cdr->billsec/60;
3871 # don't look at this service again
3872 push @seen, $h_cust_svc->svcnum;
3876 $minutes = sprintf("%d", $minutes);
3877 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3878 . "$num_deactivated Ported-Out: $num_portedout ",
3879 "Total Minutes: $minutes");
3882 sub _items_svc_phone_sections {
3891 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3892 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3894 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3895 next unless $cust_bill_pkg->pkgnum > 0;
3897 my @header = $cust_bill_pkg->details_header;
3898 next unless scalar(@header);
3900 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3902 my $phonenum = $detail->phonenum;
3903 next unless $phonenum;
3905 my $amount = $detail->amount;
3906 next unless $amount && $amount > 0;
3908 $sections{$phonenum} ||= { 'amount' => 0,
3911 'sort_weight' => -1,
3912 'phonenum' => $phonenum,
3914 $sections{$phonenum}{amount} += $amount; #subtotal
3915 $sections{$phonenum}{calls}++;
3916 $sections{$phonenum}{duration} += $detail->duration;
3918 my $desc = $detail->regionname;
3919 my $description = $desc;
3920 $description = substr($desc, 0, 50). '...'
3921 if $format eq 'latex' && length($desc) > 50;
3923 $lines{$phonenum}{$desc} ||= {
3924 description => &{$escape}($description),
3925 #pkgpart => $part_pkg->pkgpart,
3933 product_code => 'N/A',
3934 ext_description => [],
3937 $lines{$phonenum}{$desc}{amount} += $amount;
3938 $lines{$phonenum}{$desc}{calls}++;
3939 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3941 my $line = $usage_class{$detail->classnum}->classname;
3942 $sections{"$phonenum $line"} ||=
3946 'sort_weight' => $usage_class{$detail->classnum}->weight,
3947 'phonenum' => $phonenum,
3948 'header' => [ @header ],
3950 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3951 $sections{"$phonenum $line"}{calls}++;
3952 $sections{"$phonenum $line"}{duration} += $detail->duration;
3954 $lines{"$phonenum $line"}{$desc} ||= {
3955 description => &{$escape}($description),
3956 #pkgpart => $part_pkg->pkgpart,
3964 product_code => 'N/A',
3965 ext_description => [],
3968 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3969 $lines{"$phonenum $line"}{$desc}{calls}++;
3970 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3971 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3972 $detail->formatted('format' => $format);
3977 my %sectionmap = ();
3978 my $simple = new FS::usage_class { format => 'simple' }; #bleh
3979 foreach ( keys %sections ) {
3980 my @header = @{ $sections{$_}{header} || [] };
3982 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
3983 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3984 my $usage_class = $summary ? $simple : $usage_simple;
3985 my $ending = $summary ? ' usage charges' : '';
3988 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
3990 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3991 'amount' => $sections{$_}{amount}, #subtotal
3992 'calls' => $sections{$_}{calls},
3993 'duration' => $sections{$_}{duration},
3995 'tax_section' => '',
3996 'phonenum' => $sections{$_}{phonenum},
3997 'sort_weight' => $sections{$_}{sort_weight},
3998 'post_total' => $summary, #inspire pagebreak
4000 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4001 qw( description_generator
4004 total_line_generator
4011 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4012 $a->{sort_weight} <=> $b->{sort_weight}
4017 foreach my $section ( keys %lines ) {
4018 foreach my $line ( keys %{$lines{$section}} ) {
4019 my $l = $lines{$section}{$line};
4020 $l->{section} = $sectionmap{$section};
4021 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4022 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4027 return(\@sections, \@lines);
4034 #my @display = scalar(@_)
4036 # : qw( _items_previous _items_pkg );
4037 # #: qw( _items_pkg );
4038 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4039 my @display = qw( _items_previous _items_pkg );
4042 foreach my $display ( @display ) {
4043 push @b, $self->$display(@_);
4048 sub _items_previous {
4050 my $cust_main = $self->cust_main;
4051 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4053 foreach ( @pr_cust_bill ) {
4054 my $date = $conf->exists('invoice_show_prior_due_date')
4055 ? 'due '. $_->due_date2str($date_format)
4056 : time2str($date_format, $_->_date);
4058 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4059 #'pkgpart' => 'N/A',
4061 'amount' => sprintf("%.2f", $_->owed),
4067 # 'description' => 'Previous Balance',
4068 # #'pkgpart' => 'N/A',
4069 # 'pkgnum' => 'N/A',
4070 # 'amount' => sprintf("%10.2f", $pr_total ),
4071 # 'ext_description' => [ map {
4072 # "Invoice ". $_->invnum.
4073 # " (". time2str("%x",$_->_date). ") ".
4074 # sprintf("%10.2f", $_->owed)
4075 # } @pr_cust_bill ],
4083 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4084 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4085 if ($options{section} && $options{section}->{condensed}) {
4087 local $Storable::canonical = 1;
4088 foreach ( @items ) {
4090 delete $item->{ref};
4091 delete $item->{ext_description};
4092 my $key = freeze($item);
4093 $itemshash{$key} ||= 0;
4094 $itemshash{$key} ++; # += $item->{quantity};
4096 @items = sort { $a->{description} cmp $b->{description} }
4097 map { my $i = thaw($_);
4098 $i->{quantity} = $itemshash{$_};
4100 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4109 return 0 unless $a->itemdesc cmp $b->itemdesc;
4110 return -1 if $b->itemdesc eq 'Tax';
4111 return 1 if $a->itemdesc eq 'Tax';
4112 return -1 if $b->itemdesc eq 'Other surcharges';
4113 return 1 if $a->itemdesc eq 'Other surcharges';
4114 $a->itemdesc cmp $b->itemdesc;
4119 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4120 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4123 sub _items_cust_bill_pkg {
4125 my $cust_bill_pkg = shift;
4128 my $format = $opt{format} || '';
4129 my $escape_function = $opt{escape_function} || sub { shift };
4130 my $format_function = $opt{format_function} || '';
4131 my $unsquelched = $opt{unsquelched} || '';
4132 my $section = $opt{section}->{description} if $opt{section};
4133 my $summary_page = $opt{summary_page} || '';
4134 my $multilocation = $opt{multilocation} || '';
4135 my $multisection = $opt{multisection} || '';
4138 my ($s, $r, $u) = ( undef, undef, undef );
4139 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4142 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4143 if ( $_ && !$cust_bill_pkg->hidden ) {
4144 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4145 $_->{amount} =~ s/^\-0\.00$/0.00/;
4146 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4148 unless $_->{amount} == 0;
4153 foreach my $display ( grep { defined($section)
4154 ? $_->section eq $section
4157 #grep { !$_->summary || !$summary_page } # bunk!
4158 grep { !$_->summary || $multisection }
4159 $cust_bill_pkg->cust_bill_pkg_display
4163 my $type = $display->type;
4165 my $desc = $cust_bill_pkg->desc;
4166 $desc = substr($desc, 0, 50). '...'
4167 if $format eq 'latex' && length($desc) > 50;
4169 my %details_opt = ( 'format' => $format,
4170 'escape_function' => $escape_function,
4171 'format_function' => $format_function,
4174 if ( $cust_bill_pkg->pkgnum > 0 ) {
4176 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4178 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4180 my $description = $desc;
4181 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4184 unless ( $cust_pkg->part_pkg->hide_svc_detail
4185 || $cust_bill_pkg->hidden )
4188 push @d, map &{$escape_function}($_),
4189 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4190 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4192 if ( $multilocation ) {
4193 my $loc = $cust_pkg->location_label;
4194 $loc = substr($loc, 0, 50). '...'
4195 if $format eq 'latex' && length($loc) > 50;
4196 push @d, &{$escape_function}($loc);
4201 push @d, $cust_bill_pkg->details(%details_opt)
4202 if $cust_bill_pkg->recur == 0;
4204 if ( $cust_bill_pkg->hidden ) {
4205 $s->{amount} += $cust_bill_pkg->setup;
4206 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4207 push @{ $s->{ext_description} }, @d;
4210 description => $description,
4211 #pkgpart => $part_pkg->pkgpart,
4212 pkgnum => $cust_bill_pkg->pkgnum,
4213 amount => $cust_bill_pkg->setup,
4214 unit_amount => $cust_bill_pkg->unitsetup,
4215 quantity => $cust_bill_pkg->quantity,
4216 ext_description => \@d,
4222 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ) &&
4223 ( !$type || $type eq 'R' || $type eq 'U' )
4227 my $is_summary = $display->summary;
4228 my $description = ($is_summary && $type && $type eq 'U')
4229 ? "Usage charges" : $desc;
4231 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4232 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4233 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4238 #at least until cust_bill_pkg has "past" ranges in addition to
4239 #the "future" sdate/edate ones... see #3032
4240 my @dates = ( $self->_date );
4241 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4242 push @dates, $prev->sdate if $prev;
4243 push @dates, undef if !$prev;
4245 unless ( $cust_pkg->part_pkg->hide_svc_detail
4246 || $cust_bill_pkg->itemdesc
4247 || $cust_bill_pkg->hidden
4248 || $is_summary && $type && $type eq 'U' )
4251 push @d, map &{$escape_function}($_),
4252 $cust_pkg->h_labels_short(@dates, 'I')
4253 #$cust_bill_pkg->edate,
4254 #$cust_bill_pkg->sdate)
4255 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4257 if ( $multilocation ) {
4258 my $loc = $cust_pkg->location_label;
4259 $loc = substr($loc, 0, 50). '...'
4260 if $format eq 'latex' && length($loc) > 50;
4261 push @d, &{$escape_function}($loc);
4266 push @d, $cust_bill_pkg->details(%details_opt)
4267 unless ($is_summary || $type && $type eq 'R');
4271 $amount = $cust_bill_pkg->recur;
4272 }elsif($type eq 'R') {
4273 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4274 }elsif($type eq 'U') {
4275 $amount = $cust_bill_pkg->usage;
4278 if ( !$type || $type eq 'R' ) {
4280 if ( $cust_bill_pkg->hidden ) {
4281 $r->{amount} += $amount;
4282 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4283 push @{ $r->{ext_description} }, @d;
4286 description => $description,
4287 #pkgpart => $part_pkg->pkgpart,
4288 pkgnum => $cust_bill_pkg->pkgnum,
4290 unit_amount => $cust_bill_pkg->unitrecur,
4291 quantity => $cust_bill_pkg->quantity,
4292 ext_description => \@d,
4296 } else { # $type eq 'U'
4298 if ( $cust_bill_pkg->hidden ) {
4299 $u->{amount} += $amount;
4300 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4301 push @{ $u->{ext_description} }, @d;
4304 description => $description,
4305 #pkgpart => $part_pkg->pkgpart,
4306 pkgnum => $cust_bill_pkg->pkgnum,
4308 unit_amount => $cust_bill_pkg->unitrecur,
4309 quantity => $cust_bill_pkg->quantity,
4310 ext_description => \@d,
4316 } # recurring or usage with recurring charge
4318 } else { #pkgnum tax or one-shot line item (??)
4320 if ( $cust_bill_pkg->setup != 0 ) {
4322 'description' => $desc,
4323 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4326 if ( $cust_bill_pkg->recur != 0 ) {
4328 'description' => "$desc (".
4329 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4330 time2str($date_format, $cust_bill_pkg->edate). ')',
4331 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4341 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4343 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4344 $_->{amount} =~ s/^\-0\.00$/0.00/;
4345 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4347 unless $_->{amount} == 0;
4355 sub _items_credits {
4356 my( $self, %opt ) = @_;
4357 my $trim_len = $opt{'trim_len'} || 60;
4361 foreach ( $self->cust_credited ) {
4363 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4365 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4366 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4367 $reason = " ($reason) " if $reason;
4370 #'description' => 'Credit ref\#'. $_->crednum.
4371 # " (". time2str("%x",$_->cust_credit->_date) .")".
4373 'description' => 'Credit applied '.
4374 time2str($date_format,$_->cust_credit->_date). $reason,
4375 'amount' => sprintf("%.2f",$_->amount),
4383 sub _items_payments {
4387 #get & print payments
4388 foreach ( $self->cust_bill_pay ) {
4390 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4393 'description' => "Payment received ".
4394 time2str($date_format,$_->cust_pay->_date ),
4395 'amount' => sprintf("%.2f", $_->amount )
4403 =item call_details [ OPTION => VALUE ... ]
4405 Returns an array of CSV strings representing the call details for this invoice
4406 The only option available is the boolean prepend_billed_number
4411 my ($self, %opt) = @_;
4413 my $format_function = sub { shift };
4415 if ($opt{prepend_billed_number}) {
4416 $format_function = sub {
4420 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4425 my @details = map { $_->details( 'format_function' => $format_function,
4426 'escape_function' => sub{ return() },
4430 $self->cust_bill_pkg;
4431 my $header = $details[0];
4432 ( $header, grep { $_ ne $header } @details );
4442 =item process_reprint
4446 sub process_reprint {
4447 process_re_X('print', @_);
4450 =item process_reemail
4454 sub process_reemail {
4455 process_re_X('email', @_);
4463 process_re_X('fax', @_);
4471 process_re_X('ftp', @_);
4478 sub process_respool {
4479 process_re_X('spool', @_);
4482 use Storable qw(thaw);
4486 my( $method, $job ) = ( shift, shift );
4487 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4489 my $param = thaw(decode_base64(shift));
4490 warn Dumper($param) if $DEBUG;
4501 my($method, $job, %param ) = @_;
4503 warn "re_X $method for job $job with param:\n".
4504 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4507 #some false laziness w/search/cust_bill.html
4509 my $orderby = 'ORDER BY cust_bill._date';
4511 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4513 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4515 my @cust_bill = qsearch( {
4516 #'select' => "cust_bill.*",
4517 'table' => 'cust_bill',
4518 'addl_from' => $addl_from,
4520 'extra_sql' => $extra_sql,
4521 'order_by' => $orderby,
4525 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4527 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4530 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4531 foreach my $cust_bill ( @cust_bill ) {
4532 $cust_bill->$method();
4534 if ( $job ) { #progressbar foo
4536 if ( time - $min_sec > $last ) {
4537 my $error = $job->update_statustext(
4538 int( 100 * $num / scalar(@cust_bill) )
4540 die $error if $error;
4551 =head1 CLASS METHODS
4557 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4562 my ($class, $start, $end) = @_;
4564 $class->paid_sql($start, $end). ' - '.
4565 $class->credited_sql($start, $end);
4570 Returns an SQL fragment to retreive the net amount (charged minus credited).
4575 my ($class, $start, $end) = @_;
4576 'charged - '. $class->credited_sql($start, $end);
4581 Returns an SQL fragment to retreive the amount paid against this invoice.
4586 my ($class, $start, $end) = @_;
4587 $start &&= "AND cust_bill_pay._date <= $start";
4588 $end &&= "AND cust_bill_pay._date > $end";
4589 $start = '' unless defined($start);
4590 $end = '' unless defined($end);
4591 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4592 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4597 Returns an SQL fragment to retreive the amount credited against this invoice.
4602 my ($class, $start, $end) = @_;
4603 $start &&= "AND cust_credit_bill._date <= $start";
4604 $end &&= "AND cust_credit_bill._date > $end";
4605 $start = '' unless defined($start);
4606 $end = '' unless defined($end);
4607 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4608 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4613 Returns an SQL fragment to retrieve the due date of an invoice.
4614 Currently only supported on PostgreSQL.
4622 cust_bill.invoice_terms,
4623 cust_main.invoice_terms,
4624 \''.($conf->config('invoice_default_terms') || '').'\'
4625 ), E\'Net (\\\\d+)\'
4627 ) * 86400 + cust_bill._date'
4630 =item search_sql_where HASHREF
4632 Class method which returns an SQL WHERE fragment to search for parameters
4633 specified in HASHREF. Valid parameters are
4639 List reference of start date, end date, as UNIX timestamps.
4649 List reference of charged limits (exclusive).
4653 List reference of charged limits (exclusive).
4657 flag, return open invoices only
4661 flag, return net invoices only
4665 =item newest_percust
4669 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4673 sub search_sql_where {
4674 my($class, $param) = @_;
4676 warn "$me search_sql_where called with params: \n".
4677 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4683 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4684 push @search, "cust_main.agentnum = $1";
4688 if ( $param->{_date} ) {
4689 my($beginning, $ending) = @{$param->{_date}};
4691 push @search, "cust_bill._date >= $beginning",
4692 "cust_bill._date < $ending";
4696 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4697 push @search, "cust_bill.invnum >= $1";
4699 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4700 push @search, "cust_bill.invnum <= $1";
4704 if ( $param->{charged} ) {
4705 my @charged = ref($param->{charged})
4706 ? @{ $param->{charged} }
4707 : ($param->{charged});
4709 push @search, map { s/^charged/cust_bill.charged/; $_; }
4713 my $owed_sql = FS::cust_bill->owed_sql;
4716 if ( $param->{owed} ) {
4717 my @owed = ref($param->{owed})
4718 ? @{ $param->{owed} }
4720 push @search, map { s/^owed/$owed_sql/; $_; }
4725 push @search, "0 != $owed_sql"
4726 if $param->{'open'};
4727 push @search, '0 != '. FS::cust_bill->net_sql
4731 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4732 if $param->{'days'};
4735 if ( $param->{'newest_percust'} ) {
4737 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4738 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4740 my @newest_where = map { my $x = $_;
4741 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4744 grep ! /^cust_main./, @search;
4745 my $newest_where = scalar(@newest_where)
4746 ? ' AND '. join(' AND ', @newest_where)
4750 push @search, "cust_bill._date = (
4751 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4752 WHERE newest_cust_bill.custnum = cust_bill.custnum
4758 #agent virtualization
4759 my $curuser = $FS::CurrentUser::CurrentUser;
4760 if ( $curuser->username eq 'fs_queue'
4761 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4763 my $newuser = qsearchs('access_user', {
4764 'username' => $username,
4768 $curuser = $newuser;
4770 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4773 push @search, $curuser->agentnums_sql;
4775 join(' AND ', @search );
4787 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4788 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base