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
290 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
296 =item add_cc_surcharge
302 sub add_cc_surcharge {
303 my ($self, $pkgnum, $amount) = (shift, shift, shift);
306 my $cust_bill_pkg = new FS::cust_bill_pkg({
307 'invnum' => $self->invnum,
311 $error = $cust_bill_pkg->insert;
312 return $error if $error;
314 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
315 $self->charged($self->charged+$amount);
316 $error = $self->replace;
317 return $error if $error;
319 $self->apply_payments_and_credits;
325 Checks all fields to make sure this is a valid invoice. If there is an error,
326 returns the error, otherwise returns false. Called by the insert and replace
335 $self->ut_numbern('invnum')
336 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
337 || $self->ut_numbern('_date')
338 || $self->ut_money('charged')
339 || $self->ut_numbern('printed')
340 || $self->ut_enum('closed', [ '', 'Y' ])
341 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
342 || $self->ut_numbern('agent_invid') #varchar?
344 return $error if $error;
346 $self->_date(time) unless $self->_date;
348 $self->printed(0) if $self->printed eq '';
355 Returns the displayed invoice number for this invoice: agent_invid if
356 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
362 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
363 return $self->agent_invid;
365 return $self->invnum;
371 Returns a list consisting of the total previous balance for this customer,
372 followed by the previous outstanding invoices (as FS::cust_bill objects also).
379 my @cust_bill = sort { $a->_date <=> $b->_date }
380 grep { $_->owed != 0 && $_->_date < $self->_date }
381 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
383 foreach ( @cust_bill ) { $total += $_->owed; }
389 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
396 { 'table' => 'cust_bill_pkg',
397 'hashref' => { 'invnum' => $self->invnum },
398 'order_by' => 'ORDER BY billpkgnum',
403 =item cust_bill_pkg_pkgnum PKGNUM
405 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
410 sub cust_bill_pkg_pkgnum {
411 my( $self, $pkgnum ) = @_;
413 { 'table' => 'cust_bill_pkg',
414 'hashref' => { 'invnum' => $self->invnum,
417 'order_by' => 'ORDER BY billpkgnum',
424 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
431 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
432 $self->cust_bill_pkg;
434 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
439 Returns true if any of the packages (or their definitions) corresponding to the
440 line items for this invoice have the no_auto flag set.
446 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
449 =item open_cust_bill_pkg
451 Returns the open line items for this invoice.
453 Note that cust_bill_pkg with both setup and recur fees are returned as two
454 separate line items, each with only one fee.
458 # modeled after cust_main::open_cust_bill
459 sub open_cust_bill_pkg {
462 # grep { $_->owed > 0 } $self->cust_bill_pkg
464 my %other = ( 'recur' => 'setup',
465 'setup' => 'recur', );
467 foreach my $field ( qw( recur setup )) {
468 push @open, map { $_->set( $other{$field}, 0 ); $_; }
469 grep { $_->owed($field) > 0 }
470 $self->cust_bill_pkg;
476 =item cust_bill_event
478 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
482 sub cust_bill_event {
484 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
487 =item num_cust_bill_event
489 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
493 sub num_cust_bill_event {
496 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
497 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
498 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
499 $sth->fetchrow_arrayref->[0];
504 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
508 #false laziness w/cust_pkg.pm
512 'table' => 'cust_event',
513 'addl_from' => 'JOIN part_event USING ( eventpart )',
514 'hashref' => { 'tablenum' => $self->invnum },
515 'extra_sql' => " AND eventtable = 'cust_bill' ",
521 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
525 #false laziness w/cust_pkg.pm
529 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
530 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
531 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
532 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
533 $sth->fetchrow_arrayref->[0];
538 Returns the customer (see L<FS::cust_main>) for this invoice.
544 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
547 =item cust_suspend_if_balance_over AMOUNT
549 Suspends the customer associated with this invoice if the total amount owed on
550 this invoice and all older invoices is greater than the specified amount.
552 Returns a list: an empty list on success or a list of errors.
556 sub cust_suspend_if_balance_over {
557 my( $self, $amount ) = ( shift, shift );
558 my $cust_main = $self->cust_main;
559 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
562 $cust_main->suspend(@_);
568 Depreciated. See the cust_credited method.
570 #Returns a list consisting of the total previous credited (see
571 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
572 #outstanding credits (FS::cust_credit objects).
578 croak "FS::cust_bill->cust_credit depreciated; see ".
579 "FS::cust_bill->cust_credit_bill";
582 #my @cust_credit = sort { $a->_date <=> $b->_date }
583 # grep { $_->credited != 0 && $_->_date < $self->_date }
584 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
586 #foreach (@cust_credit) { $total += $_->credited; }
587 #$total, @cust_credit;
592 Depreciated. See the cust_bill_pay method.
594 #Returns all payments (see L<FS::cust_pay>) for this invoice.
600 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
602 #sort { $a->_date <=> $b->_date }
603 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
609 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
612 sub cust_bill_pay_batch {
614 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
619 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
625 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
626 sort { $a->_date <=> $b->_date }
627 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
632 =item cust_credit_bill
634 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
640 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
641 sort { $a->_date <=> $b->_date }
642 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
646 sub cust_credit_bill {
647 shift->cust_credited(@_);
650 =item cust_bill_pay_pkgnum PKGNUM
652 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
653 with matching pkgnum.
657 sub cust_bill_pay_pkgnum {
658 my( $self, $pkgnum ) = @_;
659 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
660 sort { $a->_date <=> $b->_date }
661 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
667 =item cust_credited_pkgnum PKGNUM
669 =item cust_credit_bill_pkgnum PKGNUM
671 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
672 with matching pkgnum.
676 sub cust_credited_pkgnum {
677 my( $self, $pkgnum ) = @_;
678 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
679 sort { $a->_date <=> $b->_date }
680 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
686 sub cust_credit_bill_pkgnum {
687 shift->cust_credited_pkgnum(@_);
692 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
699 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
701 foreach (@taxlines) { $total += $_->setup; }
707 Returns the amount owed (still outstanding) on this invoice, which is charged
708 minus all payment applications (see L<FS::cust_bill_pay>) and credit
709 applications (see L<FS::cust_credit_bill>).
715 my $balance = $self->charged;
716 $balance -= $_->amount foreach ( $self->cust_bill_pay );
717 $balance -= $_->amount foreach ( $self->cust_credited );
718 $balance = sprintf( "%.2f", $balance);
719 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
724 my( $self, $pkgnum ) = @_;
726 #my $balance = $self->charged;
728 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
730 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
731 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
733 $balance = sprintf( "%.2f", $balance);
734 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
738 =item apply_payments_and_credits [ OPTION => VALUE ... ]
740 Applies unapplied payments and credits to this invoice.
742 A hash of optional arguments may be passed. Currently "manual" is supported.
743 If true, a payment receipt is sent instead of a statement when
744 'payment_receipt_email' configuration option is set.
746 If there is an error, returns the error, otherwise returns false.
750 sub apply_payments_and_credits {
751 my( $self, %options ) = @_;
753 local $SIG{HUP} = 'IGNORE';
754 local $SIG{INT} = 'IGNORE';
755 local $SIG{QUIT} = 'IGNORE';
756 local $SIG{TERM} = 'IGNORE';
757 local $SIG{TSTP} = 'IGNORE';
758 local $SIG{PIPE} = 'IGNORE';
760 my $oldAutoCommit = $FS::UID::AutoCommit;
761 local $FS::UID::AutoCommit = 0;
764 $self->select_for_update; #mutex
766 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
767 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
769 if ( $conf->exists('pkg-balances') ) {
770 # limit @payments & @credits to those w/ a pkgnum grepped from $self
771 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
772 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
773 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
776 while ( $self->owed > 0 and ( @payments || @credits ) ) {
779 if ( @payments && @credits ) {
781 #decide which goes first by weight of top (unapplied) line item
783 my @open_lineitems = $self->open_cust_bill_pkg;
786 max( map { $_->part_pkg->pay_weight || 0 }
791 my $max_credit_weight =
792 max( map { $_->part_pkg->credit_weight || 0 }
798 #if both are the same... payments first? it has to be something
799 if ( $max_pay_weight >= $max_credit_weight ) {
805 } elsif ( @payments ) {
807 } elsif ( @credits ) {
810 die "guru meditation #12 and 35";
814 if ( $app eq 'pay' ) {
816 my $payment = shift @payments;
817 $unapp_amount = $payment->unapplied;
818 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
819 $app->pkgnum( $payment->pkgnum )
820 if $conf->exists('pkg-balances') && $payment->pkgnum;
822 } elsif ( $app eq 'credit' ) {
824 my $credit = shift @credits;
825 $unapp_amount = $credit->credited;
826 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
827 $app->pkgnum( $credit->pkgnum )
828 if $conf->exists('pkg-balances') && $credit->pkgnum;
831 die "guru meditation #12 and 35";
835 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
836 warn "owed_pkgnum ". $app->pkgnum;
837 $owed = $self->owed_pkgnum($app->pkgnum);
841 next unless $owed > 0;
843 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
844 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
846 $app->invnum( $self->invnum );
848 my $error = $app->insert(%options);
850 $dbh->rollback if $oldAutoCommit;
851 return "Error inserting ". $app->table. " record: $error";
853 die $error if $error;
857 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
862 =item generate_email OPTION => VALUE ...
870 sender address, required
874 alternate template name, optional
878 text attachment arrayref, optional
882 email subject, optional
886 notice name instead of "Invoice", optional
890 Returns an argument list to be passed to L<FS::Misc::send_email>.
901 my $me = '[FS::cust_bill::generate_email]';
904 'from' => $args{'from'},
905 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
909 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
910 'template' => $args{'template'},
911 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
914 my $cust_main = $self->cust_main;
916 if (ref($args{'to'}) eq 'ARRAY') {
917 $return{'to'} = $args{'to'};
919 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
920 $cust_main->invoicing_list
924 if ( $conf->exists('invoice_html') ) {
926 warn "$me creating HTML/text multipart message"
929 $return{'nobody'} = 1;
931 my $alternative = build MIME::Entity
932 'Type' => 'multipart/alternative',
933 'Encoding' => '7bit',
934 'Disposition' => 'inline'
938 if ( $conf->exists('invoice_email_pdf')
939 and scalar($conf->config('invoice_email_pdf_note')) ) {
941 warn "$me using 'invoice_email_pdf_note' in multipart message"
943 $data = [ map { $_ . "\n" }
944 $conf->config('invoice_email_pdf_note')
949 warn "$me not using 'invoice_email_pdf_note' in multipart message"
951 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
952 $data = $args{'print_text'};
954 $data = [ $self->print_text(\%opt) ];
959 $alternative->attach(
960 'Type' => 'text/plain',
961 #'Encoding' => 'quoted-printable',
962 'Encoding' => '7bit',
964 'Disposition' => 'inline',
967 $args{'from'} =~ /\@([\w\.\-]+)/;
968 my $from = $1 || 'example.com';
969 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
972 my $agentnum = $cust_main->agentnum;
973 if ( defined($args{'template'}) && length($args{'template'})
974 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
977 $logo = 'logo_'. $args{'template'}. '.png';
981 my $image_data = $conf->config_binary( $logo, $agentnum);
983 my $image = build MIME::Entity
984 'Type' => 'image/png',
985 'Encoding' => 'base64',
986 'Data' => $image_data,
987 'Filename' => 'logo.png',
988 'Content-ID' => "<$content_id>",
991 $alternative->attach(
992 'Type' => 'text/html',
993 'Encoding' => 'quoted-printable',
994 'Data' => [ '<html>',
997 ' '. encode_entities($return{'subject'}),
1000 ' <body bgcolor="#e8e8e8">',
1001 $self->print_html({ 'cid'=>$content_id, %opt }),
1005 'Disposition' => 'inline',
1006 #'Filename' => 'invoice.pdf',
1009 my @otherparts = ();
1010 if ( $cust_main->email_csv_cdr ) {
1012 push @otherparts, build MIME::Entity
1013 'Type' => 'text/csv',
1014 'Encoding' => '7bit',
1015 'Data' => [ map { "$_\n" }
1016 $self->call_details('prepend_billed_number' => 1)
1018 'Disposition' => 'attachment',
1019 'Filename' => 'usage-'. $self->invnum. '.csv',
1024 if ( $conf->exists('invoice_email_pdf') ) {
1029 # multipart/alternative
1035 my $related = build MIME::Entity 'Type' => 'multipart/related',
1036 'Encoding' => '7bit';
1038 #false laziness w/Misc::send_email
1039 $related->head->replace('Content-type',
1040 $related->mime_type.
1041 '; boundary="'. $related->head->multipart_boundary. '"'.
1042 '; type=multipart/alternative'
1045 $related->add_part($alternative);
1047 $related->add_part($image);
1049 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1051 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1055 #no other attachment:
1057 # multipart/alternative
1062 $return{'content-type'} = 'multipart/related';
1063 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1064 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1065 #$return{'disposition'} = 'inline';
1071 if ( $conf->exists('invoice_email_pdf') ) {
1072 warn "$me creating PDF attachment"
1075 #mime parts arguments a la MIME::Entity->build().
1076 $return{'mimeparts'} = [
1077 { $self->mimebuild_pdf(\%opt) }
1081 if ( $conf->exists('invoice_email_pdf')
1082 and scalar($conf->config('invoice_email_pdf_note')) ) {
1084 warn "$me using 'invoice_email_pdf_note'"
1086 $return{'body'} = [ map { $_ . "\n" }
1087 $conf->config('invoice_email_pdf_note')
1092 warn "$me not using 'invoice_email_pdf_note'"
1094 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1095 $return{'body'} = $args{'print_text'};
1097 $return{'body'} = [ $self->print_text(\%opt) ];
1110 Returns a list suitable for passing to MIME::Entity->build(), representing
1111 this invoice as PDF attachment.
1118 'Type' => 'application/pdf',
1119 'Encoding' => 'base64',
1120 'Data' => [ $self->print_pdf(@_) ],
1121 'Disposition' => 'attachment',
1122 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1126 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1128 Sends this invoice to the destinations configured for this customer: sends
1129 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1131 Options can be passed as a hashref (recommended) or as a list of up to
1132 four values for templatename, agentnum, invoice_from and amount.
1134 I<template>, if specified, is the name of a suffix for alternate invoices.
1136 I<agentnum>, if specified, means that this invoice will only be sent for customers
1137 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1138 single agent) or an arrayref of agentnums.
1140 I<invoice_from>, if specified, overrides the default email invoice From: address.
1142 I<amount>, if specified, only sends the invoice if the total amount owed on this
1143 invoice and all older invoices is greater than the specified amount.
1145 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1149 sub queueable_send {
1152 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1153 or die "invalid invoice number: " . $opt{invnum};
1155 my @args = ( $opt{template}, $opt{agentnum} );
1156 push @args, $opt{invoice_from}
1157 if exists($opt{invoice_from}) && $opt{invoice_from};
1159 my $error = $self->send( @args );
1160 die $error if $error;
1167 my( $template, $invoice_from, $notice_name );
1169 my $balance_over = 0;
1173 $template = $opt->{'template'} || '';
1174 if ( $agentnums = $opt->{'agentnum'} ) {
1175 $agentnums = [ $agentnums ] unless ref($agentnums);
1177 $invoice_from = $opt->{'invoice_from'};
1178 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1179 $notice_name = $opt->{'notice_name'};
1181 $template = scalar(@_) ? shift : '';
1182 if ( scalar(@_) && $_[0] ) {
1183 $agentnums = ref($_[0]) ? shift : [ shift ];
1185 $invoice_from = shift if scalar(@_);
1186 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1189 return 'N/A' unless ! $agentnums
1190 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1193 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1195 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1196 $conf->config('invoice_from', $self->cust_main->agentnum );
1199 'template' => $template,
1200 'invoice_from' => $invoice_from,
1201 'notice_name' => ( $notice_name || 'Invoice' ),
1204 my @invoicing_list = $self->cust_main->invoicing_list;
1206 #$self->email_invoice(\%opt)
1208 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1210 #$self->print_invoice(\%opt)
1212 if grep { $_ eq 'POST' } @invoicing_list; #postal
1214 $self->fax_invoice(\%opt)
1215 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1221 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1223 Emails this invoice.
1225 Options can be passed as a hashref (recommended) or as a list of up to
1226 two values for templatename and invoice_from.
1228 I<template>, if specified, is the name of a suffix for alternate invoices.
1230 I<invoice_from>, if specified, overrides the default email invoice From: address.
1232 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1236 sub queueable_email {
1239 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1240 or die "invalid invoice number: " . $opt{invnum};
1242 my @args = ( $opt{template} );
1243 push @args, $opt{invoice_from}
1244 if exists($opt{invoice_from}) && $opt{invoice_from};
1246 my $error = $self->email( @args );
1247 die $error if $error;
1251 #sub email_invoice {
1255 my( $template, $invoice_from, $notice_name );
1258 $template = $opt->{'template'} || '';
1259 $invoice_from = $opt->{'invoice_from'};
1260 $notice_name = $opt->{'notice_name'} || 'Invoice';
1262 $template = scalar(@_) ? shift : '';
1263 $invoice_from = shift if scalar(@_);
1264 $notice_name = 'Invoice';
1267 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1268 $conf->config('invoice_from', $self->cust_main->agentnum );
1270 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1271 $self->cust_main->invoicing_list;
1273 if ( ! @invoicing_list ) { #no recipients
1274 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1275 die 'No recipients for customer #'. $self->custnum;
1277 #default: better to notify this person than silence
1278 @invoicing_list = ($invoice_from);
1282 my $subject = $self->email_subject($template);
1284 my $error = send_email(
1285 $self->generate_email(
1286 'from' => $invoice_from,
1287 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1288 'subject' => $subject,
1289 'template' => $template,
1290 'notice_name' => $notice_name,
1293 die "can't email invoice: $error\n" if $error;
1294 #die "$error\n" if $error;
1301 #my $template = scalar(@_) ? shift : '';
1304 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1307 my $cust_main = $self->cust_main;
1308 my $name = $cust_main->name;
1309 my $name_short = $cust_main->name_short;
1310 my $invoice_number = $self->invnum;
1311 my $invoice_date = $self->_date_pretty;
1313 eval qq("$subject");
1316 =item lpr_data HASHREF | [ TEMPLATE ]
1318 Returns the postscript or plaintext for this invoice as an arrayref.
1320 Options can be passed as a hashref (recommended) or as a single optional value
1323 I<template>, if specified, is the name of a suffix for alternate invoices.
1325 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1331 my( $template, $notice_name );
1334 $template = $opt->{'template'} || '';
1335 $notice_name = $opt->{'notice_name'} || 'Invoice';
1337 $template = scalar(@_) ? shift : '';
1338 $notice_name = 'Invoice';
1342 'template' => $template,
1343 'notice_name' => $notice_name,
1346 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1347 [ $self->$method( \%opt ) ];
1350 =item print HASHREF | [ TEMPLATE ]
1352 Prints this invoice.
1354 Options can be passed as a hashref (recommended) or as a single optional
1357 I<template>, if specified, is the name of a suffix for alternate invoices.
1359 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1363 #sub print_invoice {
1366 my( $template, $notice_name );
1369 $template = $opt->{'template'} || '';
1370 $notice_name = $opt->{'notice_name'} || 'Invoice';
1372 $template = scalar(@_) ? shift : '';
1373 $notice_name = 'Invoice';
1377 'template' => $template,
1378 'notice_name' => $notice_name,
1381 if($conf->exists('invoice_print_pdf')) {
1382 # Add the invoice to the current batch.
1383 $self->batch_invoice(\%opt);
1386 do_print $self->lpr_data(\%opt);
1390 =item fax_invoice HASHREF | [ TEMPLATE ]
1394 Options can be passed as a hashref (recommended) or as a single optional
1397 I<template>, if specified, is the name of a suffix for alternate invoices.
1399 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1405 my( $template, $notice_name );
1408 $template = $opt->{'template'} || '';
1409 $notice_name = $opt->{'notice_name'} || 'Invoice';
1411 $template = scalar(@_) ? shift : '';
1412 $notice_name = 'Invoice';
1415 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1416 unless $conf->exists('invoice_latex');
1418 my $dialstring = $self->cust_main->getfield('fax');
1422 'template' => $template,
1423 'notice_name' => $notice_name,
1426 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1427 'dialstring' => $dialstring,
1429 die $error if $error;
1433 =item batch_invoice [ HASHREF ]
1435 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1436 isn't an open batch, one will be created.
1441 my ($self, $opt) = @_;
1442 my $batch = FS::bill_batch->get_open_batch;
1443 my $cust_bill_batch = FS::cust_bill_batch->new({
1444 batchnum => $batch->batchnum,
1445 invnum => $self->invnum,
1447 return $cust_bill_batch->insert($opt);
1450 =item ftp_invoice [ TEMPLATENAME ]
1452 Sends this invoice data via FTP.
1454 TEMPLATENAME is unused?
1460 my $template = scalar(@_) ? shift : '';
1463 'protocol' => 'ftp',
1464 'server' => $conf->config('cust_bill-ftpserver'),
1465 'username' => $conf->config('cust_bill-ftpusername'),
1466 'password' => $conf->config('cust_bill-ftppassword'),
1467 'dir' => $conf->config('cust_bill-ftpdir'),
1468 'format' => $conf->config('cust_bill-ftpformat'),
1472 =item spool_invoice [ TEMPLATENAME ]
1474 Spools this invoice data (see L<FS::spool_csv>)
1476 TEMPLATENAME is unused?
1482 my $template = scalar(@_) ? shift : '';
1485 'format' => $conf->config('cust_bill-spoolformat'),
1486 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1490 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1492 Like B<send>, but only sends the invoice if it is the newest open invoice for
1497 sub send_if_newest {
1502 grep { $_->owed > 0 }
1503 qsearch('cust_bill', {
1504 'custnum' => $self->custnum,
1505 #'_date' => { op=>'>', value=>$self->_date },
1506 'invnum' => { op=>'>', value=>$self->invnum },
1513 =item send_csv OPTION => VALUE, ...
1515 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1519 protocol - currently only "ftp"
1525 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1526 and YYMMDDHHMMSS is a timestamp.
1528 See L</print_csv> for a description of the output format.
1533 my($self, %opt) = @_;
1537 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1538 mkdir $spooldir, 0700 unless -d $spooldir;
1540 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1541 my $file = "$spooldir/$tracctnum.csv";
1543 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1545 open(CSV, ">$file") or die "can't open $file: $!";
1553 if ( $opt{protocol} eq 'ftp' ) {
1554 eval "use Net::FTP;";
1556 $net = Net::FTP->new($opt{server}) or die @$;
1558 die "unknown protocol: $opt{protocol}";
1561 $net->login( $opt{username}, $opt{password} )
1562 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1564 $net->binary or die "can't set binary mode";
1566 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1568 $net->put($file) or die "can't put $file: $!";
1578 Spools CSV invoice data.
1584 =item format - 'default' or 'billco'
1586 =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>).
1588 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1590 =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.
1597 my($self, %opt) = @_;
1599 my $cust_main = $self->cust_main;
1601 if ( $opt{'dest'} ) {
1602 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1603 $cust_main->invoicing_list;
1604 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1605 || ! keys %invoicing_list;
1608 if ( $opt{'balanceover'} ) {
1610 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1613 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1614 mkdir $spooldir, 0700 unless -d $spooldir;
1616 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1620 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1621 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1624 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1626 open(CSV, ">>$file") or die "can't open $file: $!";
1627 flock(CSV, LOCK_EX);
1632 if ( lc($opt{'format'}) eq 'billco' ) {
1634 flock(CSV, LOCK_UN);
1639 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1642 open(CSV,">>$file") or die "can't open $file: $!";
1643 flock(CSV, LOCK_EX);
1649 flock(CSV, LOCK_UN);
1656 =item print_csv OPTION => VALUE, ...
1658 Returns CSV data for this invoice.
1662 format - 'default' or 'billco'
1664 Returns a list consisting of two scalars. The first is a single line of CSV
1665 header information for this invoice. The second is one or more lines of CSV
1666 detail information for this invoice.
1668 If I<format> is not specified or "default", the fields of the CSV file are as
1671 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1675 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1677 B<record_type> is C<cust_bill> for the initial header line only. The
1678 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1679 fields are filled in.
1681 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1682 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1685 =item invnum - invoice number
1687 =item custnum - customer number
1689 =item _date - invoice date
1691 =item charged - total invoice amount
1693 =item first - customer first name
1695 =item last - customer first name
1697 =item company - company name
1699 =item address1 - address line 1
1701 =item address2 - address line 1
1711 =item pkg - line item description
1713 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1715 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1717 =item sdate - start date for recurring fee
1719 =item edate - end date for recurring fee
1723 If I<format> is "billco", the fields of the header CSV file are as follows:
1725 +-------------------------------------------------------------------+
1726 | FORMAT HEADER FILE |
1727 |-------------------------------------------------------------------|
1728 | Field | Description | Name | Type | Width |
1729 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1730 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1731 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1732 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1733 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1734 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1735 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1736 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1737 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1738 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1739 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1740 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1741 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1742 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1743 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1744 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1745 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1746 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1747 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1748 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1749 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1750 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1751 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1752 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1753 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1754 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1755 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1756 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1757 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1758 +-------+-------------------------------+------------+------+-------+
1760 If I<format> is "billco", the fields of the detail CSV file are as follows:
1762 FORMAT FOR DETAIL FILE
1764 Field | Description | Name | Type | Width
1765 1 | N/A-Leave Empty | RC | CHAR | 2
1766 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1767 3 | Account Number | TRACCTNUM | CHAR | 15
1768 4 | Invoice Number | TRINVOICE | CHAR | 15
1769 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1770 6 | Transaction Detail | DETAILS | CHAR | 100
1771 7 | Amount | AMT | NUM* | 9
1772 8 | Line Format Control** | LNCTRL | CHAR | 2
1773 9 | Grouping Code | GROUP | CHAR | 2
1774 10 | User Defined | ACCT CODE | CHAR | 15
1779 my($self, %opt) = @_;
1781 eval "use Text::CSV_XS";
1784 my $cust_main = $self->cust_main;
1786 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1788 if ( lc($opt{'format'}) eq 'billco' ) {
1791 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1793 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1795 my( $previous_balance, @unused ) = $self->previous; #previous balance
1797 my $pmt_cr_applied = 0;
1798 $pmt_cr_applied += $_->{'amount'}
1799 foreach ( $self->_items_payments, $self->_items_credits ) ;
1801 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1804 '', # 1 | N/A-Leave Empty CHAR 2
1805 '', # 2 | N/A-Leave Empty CHAR 15
1806 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1807 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1808 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1809 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1810 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1811 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1812 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1813 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1814 '', # 10 | Ancillary Billing Information CHAR 30
1815 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1816 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1819 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1822 $duedate, # 14 | Bill Due Date CHAR 10
1824 $previous_balance, # 15 | Previous Balance NUM* 9
1825 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1826 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1827 $totaldue, # 18 | Total Amt Due NUM* 9
1828 $totaldue, # 19 | Total Amt Due NUM* 9
1829 '', # 20 | 30 Day Aging NUM* 9
1830 '', # 21 | 60 Day Aging NUM* 9
1831 '', # 22 | 90 Day Aging NUM* 9
1832 'N', # 23 | Y/N CHAR 1
1833 '', # 24 | Remittance automation CHAR 100
1834 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1835 $self->custnum, # 26 | Customer Reference Number CHAR 15
1836 '0', # 27 | Federal Tax*** NUM* 9
1837 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1838 '0', # 29 | Other Taxes & Fees*** NUM* 9
1847 time2str("%x", $self->_date),
1848 sprintf("%.2f", $self->charged),
1849 ( map { $cust_main->getfield($_) }
1850 qw( first last company address1 address2 city state zip country ) ),
1852 ) or die "can't create csv";
1855 my $header = $csv->string. "\n";
1858 if ( lc($opt{'format'}) eq 'billco' ) {
1861 foreach my $item ( $self->_items_pkg ) {
1864 '', # 1 | N/A-Leave Empty CHAR 2
1865 '', # 2 | N/A-Leave Empty CHAR 15
1866 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1867 $self->invnum, # 4 | Invoice Number CHAR 15
1868 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1869 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1870 $item->{'amount'}, # 7 | Amount NUM* 9
1871 '', # 8 | Line Format Control** CHAR 2
1872 '', # 9 | Grouping Code CHAR 2
1873 '', # 10 | User Defined CHAR 15
1876 $detail .= $csv->string. "\n";
1882 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1884 my($pkg, $setup, $recur, $sdate, $edate);
1885 if ( $cust_bill_pkg->pkgnum ) {
1887 ($pkg, $setup, $recur, $sdate, $edate) = (
1888 $cust_bill_pkg->part_pkg->pkg,
1889 ( $cust_bill_pkg->setup != 0
1890 ? sprintf("%.2f", $cust_bill_pkg->setup )
1892 ( $cust_bill_pkg->recur != 0
1893 ? sprintf("%.2f", $cust_bill_pkg->recur )
1895 ( $cust_bill_pkg->sdate
1896 ? time2str("%x", $cust_bill_pkg->sdate)
1898 ($cust_bill_pkg->edate
1899 ?time2str("%x", $cust_bill_pkg->edate)
1903 } else { #pkgnum tax
1904 next unless $cust_bill_pkg->setup != 0;
1905 $pkg = $cust_bill_pkg->desc;
1906 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1907 ( $sdate, $edate ) = ( '', '' );
1913 ( map { '' } (1..11) ),
1914 ($pkg, $setup, $recur, $sdate, $edate)
1915 ) or die "can't create csv";
1917 $detail .= $csv->string. "\n";
1923 ( $header, $detail );
1929 Pays this invoice with a compliemntary payment. If there is an error,
1930 returns the error, otherwise returns false.
1936 my $cust_pay = new FS::cust_pay ( {
1937 'invnum' => $self->invnum,
1938 'paid' => $self->owed,
1941 'payinfo' => $self->cust_main->payinfo,
1949 Attempts to pay this invoice with a credit card payment via a
1950 Business::OnlinePayment realtime gateway. See
1951 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1952 for supported processors.
1958 $self->realtime_bop( 'CC', @_ );
1963 Attempts to pay this invoice with an electronic check (ACH) payment via a
1964 Business::OnlinePayment realtime gateway. See
1965 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1966 for supported processors.
1972 $self->realtime_bop( 'ECHECK', @_ );
1977 Attempts to pay this invoice with phone bill (LEC) payment via a
1978 Business::OnlinePayment realtime gateway. See
1979 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1980 for supported processors.
1986 $self->realtime_bop( 'LEC', @_ );
1990 my( $self, $method ) = (shift,shift);
1993 my $cust_main = $self->cust_main;
1994 my $balance = $cust_main->balance;
1995 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1996 $amount = sprintf("%.2f", $amount);
1997 return "not run (balance $balance)" unless $amount > 0;
1999 my $description = 'Internet Services';
2000 if ( $conf->exists('business-onlinepayment-description') ) {
2001 my $dtempl = $conf->config('business-onlinepayment-description');
2003 my $agent_obj = $cust_main->agent
2004 or die "can't retreive agent for $cust_main (agentnum ".
2005 $cust_main->agentnum. ")";
2006 my $agent = $agent_obj->agent;
2007 my $pkgs = join(', ',
2008 map { $_->part_pkg->pkg }
2009 grep { $_->pkgnum } $self->cust_bill_pkg
2011 $description = eval qq("$dtempl");
2014 $cust_main->realtime_bop($method, $amount,
2015 'description' => $description,
2016 'invnum' => $self->invnum,
2017 #this didn't do what we want, it just calls apply_payments_and_credits
2019 'apply_to_invoice' => 1,
2022 #this changes application behavior: auto payments
2023 #triggered against a specific invoice are now applied
2024 #to that invoice instead of oldest open.
2030 =item batch_card OPTION => VALUE...
2032 Adds a payment for this invoice to the pending credit card batch (see
2033 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2034 runs the payment using a realtime gateway.
2039 my ($self, %options) = @_;
2040 my $cust_main = $self->cust_main;
2042 $options{invnum} = $self->invnum;
2044 $cust_main->batch_card(%options);
2047 sub _agent_template {
2049 $self->cust_main->agent_template;
2052 sub _agent_invoice_from {
2054 $self->cust_main->agent_invoice_from;
2057 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2059 Returns an text invoice, as a list of lines.
2061 Options can be passed as a hashref (recommended) or as a list of time, template
2062 and then any key/value pairs for any other options.
2064 I<time>, if specified, is used to control the printing of overdue messages. The
2065 default is now. It isn't the date of the invoice; that's the `_date' field.
2066 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2067 L<Time::Local> and L<Date::Parse> for conversion functions.
2069 I<template>, if specified, is the name of a suffix for alternate invoices.
2071 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2077 my( $today, $template, %opt );
2079 %opt = %{ shift() };
2080 $today = delete($opt{'time'}) || '';
2081 $template = delete($opt{template}) || '';
2083 ( $today, $template, %opt ) = @_;
2086 my %params = ( 'format' => 'template' );
2087 $params{'time'} = $today if $today;
2088 $params{'template'} = $template if $template;
2089 $params{$_} = $opt{$_}
2090 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2092 $self->print_generic( %params );
2095 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2097 Internal method - returns a filename of a filled-in LaTeX template for this
2098 invoice (Note: add ".tex" to get the actual filename), and a filename of
2099 an associated logo (with the .eps extension included).
2101 See print_ps and print_pdf for methods that return PostScript and PDF output.
2103 Options can be passed as a hashref (recommended) or as a list of time, template
2104 and then any key/value pairs for any other options.
2106 I<time>, if specified, is used to control the printing of overdue messages. The
2107 default is now. It isn't the date of the invoice; that's the `_date' field.
2108 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2109 L<Time::Local> and L<Date::Parse> for conversion functions.
2111 I<template>, if specified, is the name of a suffix for alternate invoices.
2113 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2119 my( $today, $template, %opt );
2121 %opt = %{ shift() };
2122 $today = delete($opt{'time'}) || '';
2123 $template = delete($opt{template}) || '';
2125 ( $today, $template, %opt ) = @_;
2128 my %params = ( 'format' => 'latex' );
2129 $params{'time'} = $today if $today;
2130 $params{'template'} = $template if $template;
2131 $params{$_} = $opt{$_}
2132 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2134 $template ||= $self->_agent_template;
2136 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2137 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2141 ) or die "can't open temp file: $!\n";
2143 my $agentnum = $self->cust_main->agentnum;
2145 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2146 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2147 or die "can't write temp file: $!\n";
2149 print $lh $conf->config_binary('logo.eps', $agentnum)
2150 or die "can't write temp file: $!\n";
2153 $params{'logo_file'} = $lh->filename;
2155 my @filled_in = $self->print_generic( %params );
2157 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2161 ) or die "can't open temp file: $!\n";
2162 print $fh join('', @filled_in );
2165 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2166 return ($1, $params{'logo_file'});
2170 =item print_generic OPTION => VALUE ...
2172 Internal method - returns a filled-in template for this invoice as a scalar.
2174 See print_ps and print_pdf for methods that return PostScript and PDF output.
2176 Non optional options include
2177 format - latex, html, template
2179 Optional options include
2181 template - a value used as a suffix for a configuration template
2183 time - a value used to control the printing of overdue messages. The
2184 default is now. It isn't the date of the invoice; that's the `_date' field.
2185 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2186 L<Time::Local> and L<Date::Parse> for conversion functions.
2190 unsquelch_cdr - overrides any per customer cdr squelching when true
2192 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2196 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2197 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2198 # yes: fixed width (dot matrix) text printing will be borked
2201 my( $self, %params ) = @_;
2202 my $today = $params{today} ? $params{today} : time;
2203 warn "$me print_generic called on $self with suffix $params{template}\n"
2206 my $format = $params{format};
2207 die "Unknown format: $format"
2208 unless $format =~ /^(latex|html|template)$/;
2210 my $cust_main = $self->cust_main;
2211 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2212 unless $cust_main->payname
2213 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2215 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2216 'html' => [ '<%=', '%>' ],
2217 'template' => [ '{', '}' ],
2220 warn "$me print_generic creating template\n"
2223 #create the template
2224 my $template = $params{template} ? $params{template} : $self->_agent_template;
2225 my $templatefile = "invoice_$format";
2226 $templatefile .= "_$template"
2227 if length($template);
2228 my @invoice_template = map "$_\n", $conf->config($templatefile)
2229 or die "cannot load config data $templatefile";
2232 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2233 #change this to a die when the old code is removed
2234 warn "old-style invoice template $templatefile; ".
2235 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2236 $old_latex = 'true';
2237 @invoice_template = _translate_old_latex_format(@invoice_template);
2240 warn "$me print_generic creating T:T object\n"
2243 my $text_template = new Text::Template(
2245 SOURCE => \@invoice_template,
2246 DELIMITERS => $delimiters{$format},
2249 warn "$me print_generic compiling T:T object\n"
2252 $text_template->compile()
2253 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2256 # additional substitution could possibly cause breakage in existing templates
2257 my %convert_maps = (
2259 'notes' => sub { map "$_", @_ },
2260 'footer' => sub { map "$_", @_ },
2261 'smallfooter' => sub { map "$_", @_ },
2262 'returnaddress' => sub { map "$_", @_ },
2263 'coupon' => sub { map "$_", @_ },
2264 'summary' => sub { map "$_", @_ },
2270 s/%%(.*)$/<!-- $1 -->/g;
2271 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2272 s/\\begin\{enumerate\}/<ol>/g;
2274 s/\\end\{enumerate\}/<\/ol>/g;
2275 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2284 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2286 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2291 s/\\\\\*?\s*$/<BR>/;
2292 s/\\hyphenation\{[\w\s\-]+}//;
2297 'coupon' => sub { "" },
2298 'summary' => sub { "" },
2305 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2306 s/\\begin\{enumerate\}//g;
2308 s/\\end\{enumerate\}//g;
2309 s/\\textbf\{(.*)\}/$1/g;
2316 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2318 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2323 s/\\\\\*?\s*$/\n/; # dubious
2324 s/\\hyphenation\{[\w\s\-]+}//;
2328 'coupon' => sub { "" },
2329 'summary' => sub { "" },
2334 # hashes for differing output formats
2335 my %nbsps = ( 'latex' => '~',
2336 'html' => '', # '&nbps;' would be nice
2337 'template' => '', # not used
2339 my $nbsp = $nbsps{$format};
2341 my %escape_functions = ( 'latex' => \&_latex_escape,
2342 'html' => \&_html_escape_nbsp,#\&encode_entities,
2343 'template' => sub { shift },
2345 my $escape_function = $escape_functions{$format};
2346 my $escape_function_nonbsp = ($format eq 'html')
2347 ? \&_html_escape : $escape_function;
2349 my %date_formats = ( 'latex' => $date_format_long,
2350 'html' => $date_format_long,
2353 $date_formats{'html'} =~ s/ / /g;
2355 my $date_format = $date_formats{$format};
2357 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2359 'html' => sub { return '<b>'. shift(). '</b>'
2361 'template' => sub { shift },
2363 my $embolden_function = $embolden_functions{$format};
2365 warn "$me generating template variables\n"
2368 # generate template variables
2371 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2375 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2381 $returnaddress = join("\n",
2382 $conf->config_orbase("invoice_${format}returnaddress", $template)
2385 } elsif ( grep /\S/,
2386 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2388 my $convert_map = $convert_maps{$format}{'returnaddress'};
2391 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2396 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2398 my $convert_map = $convert_maps{$format}{'returnaddress'};
2399 $returnaddress = join( "\n", &$convert_map(
2400 map { s/( {2,})/'~' x length($1)/eg;
2404 ( $conf->config('company_name', $self->cust_main->agentnum),
2405 $conf->config('company_address', $self->cust_main->agentnum),
2412 my $warning = "Couldn't find a return address; ".
2413 "do you need to set the company_address configuration value?";
2415 $returnaddress = $nbsp;
2416 #$returnaddress = $warning;
2420 warn "$me generating invoice data\n"
2423 my $agentnum = $self->cust_main->agentnum;
2425 my %invoice_data = (
2428 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2429 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2430 'returnaddress' => $returnaddress,
2431 'agent' => &$escape_function($cust_main->agent->agent),
2434 'invnum' => $self->invnum,
2435 'date' => time2str($date_format, $self->_date),
2436 'today' => time2str($date_format_long, $today),
2437 'terms' => $self->terms,
2438 'template' => $template, #params{'template'},
2439 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2440 'current_charges' => sprintf("%.2f", $self->charged),
2441 'duedate' => $self->due_date2str($rdate_format), #date_format?
2444 'custnum' => $cust_main->display_custnum,
2445 'agent_custid' => &$escape_function($cust_main->agent_custid),
2446 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2447 payname company address1 address2 city state zip fax
2451 'ship_enable' => $conf->exists('invoice-ship_address'),
2452 'unitprices' => $conf->exists('invoice-unitprice'),
2453 'smallernotes' => $conf->exists('invoice-smallernotes'),
2454 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2455 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2457 #layout info -- would be fancy to calc some of this and bury the template
2459 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2460 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2461 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2462 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2463 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2464 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2465 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2466 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2467 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2468 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2470 # better hang on to conf_dir for a while (for old templates)
2471 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2473 #these are only used when doing paged plaintext
2479 $invoice_data{finance_section} = '';
2480 if ( $conf->config('finance_pkgclass') ) {
2482 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2483 $invoice_data{finance_section} = $pkg_class->categoryname;
2485 $invoice_data{finance_amount} = '0.00';
2486 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2488 my $countrydefault = $conf->config('countrydefault') || 'US';
2489 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2490 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2491 my $method = $prefix.$_;
2492 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2494 $invoice_data{'ship_country'} = ''
2495 if ( $invoice_data{'ship_country'} eq $countrydefault );
2497 $invoice_data{'cid'} = $params{'cid'}
2500 if ( $cust_main->country eq $countrydefault ) {
2501 $invoice_data{'country'} = '';
2503 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2507 $invoice_data{'address'} = \@address;
2509 $cust_main->payname.
2510 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2511 ? " (P.O. #". $cust_main->payinfo. ")"
2515 push @address, $cust_main->company
2516 if $cust_main->company;
2517 push @address, $cust_main->address1;
2518 push @address, $cust_main->address2
2519 if $cust_main->address2;
2521 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2522 push @address, $invoice_data{'country'}
2523 if $invoice_data{'country'};
2525 while (scalar(@address) < 5);
2527 $invoice_data{'logo_file'} = $params{'logo_file'}
2528 if $params{'logo_file'};
2530 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2531 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2532 #my $balance_due = $self->owed + $pr_total - $cr_total;
2533 my $balance_due = $self->owed + $pr_total;
2534 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2535 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2536 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2537 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2539 my $summarypage = '';
2540 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2543 $invoice_data{'summarypage'} = $summarypage;
2545 warn "$me substituting variables in notes, footer, smallfooter\n"
2548 foreach my $include (qw( notes footer smallfooter coupon )) {
2550 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2553 if ( $conf->exists($inc_file, $agentnum)
2554 && length( $conf->config($inc_file, $agentnum) ) ) {
2556 @inc_src = $conf->config($inc_file, $agentnum);
2560 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2562 my $convert_map = $convert_maps{$format}{$include};
2564 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2565 s/--\@\]/$delimiters{$format}[1]/g;
2568 &$convert_map( $conf->config($inc_file, $agentnum) );
2572 my $inc_tt = new Text::Template (
2574 SOURCE => [ map "$_\n", @inc_src ],
2575 DELIMITERS => $delimiters{$format},
2576 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2578 unless ( $inc_tt->compile() ) {
2579 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2580 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2584 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2586 $invoice_data{$include} =~ s/\n+$//
2587 if ($format eq 'latex');
2590 $invoice_data{'po_line'} =
2591 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2592 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2595 my %money_chars = ( 'latex' => '',
2596 'html' => $conf->config('money_char') || '$',
2599 my $money_char = $money_chars{$format};
2601 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2602 'html' => $conf->config('money_char') || '$',
2605 my $other_money_char = $other_money_chars{$format};
2606 $invoice_data{'dollar'} = $other_money_char;
2608 my @detail_items = ();
2609 my @total_items = ();
2613 $invoice_data{'detail_items'} = \@detail_items;
2614 $invoice_data{'total_items'} = \@total_items;
2615 $invoice_data{'buf'} = \@buf;
2616 $invoice_data{'sections'} = \@sections;
2618 warn "$me generating sections\n"
2621 my $previous_section = { 'description' => 'Previous Charges',
2622 'subtotal' => $other_money_char.
2623 sprintf('%.2f', $pr_total),
2624 'summarized' => $summarypage ? 'Y' : '',
2626 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2627 join(' / ', map { $cust_main->balance_date_range(@$_) }
2628 $self->_prior_month30s
2630 if $conf->exists('invoice_include_aging');
2633 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2634 'subtotal' => $taxtotal, # adjusted below
2635 'summarized' => $summarypage ? 'Y' : '',
2637 my $tax_weight = _pkg_category($tax_section->{description})
2638 ? _pkg_category($tax_section->{description})->weight
2640 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2641 $tax_section->{'sort_weight'} = $tax_weight;
2644 my $adjusttotal = 0;
2645 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2646 'subtotal' => 0, # adjusted below
2647 'summarized' => $summarypage ? 'Y' : '',
2649 my $adjust_weight = _pkg_category($adjust_section->{description})
2650 ? _pkg_category($adjust_section->{description})->weight
2652 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2653 $adjust_section->{'sort_weight'} = $adjust_weight;
2655 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2656 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2657 $invoice_data{'multisection'} = $multisection;
2658 my $late_sections = [];
2659 my $extra_sections = [];
2660 my $extra_lines = ();
2661 if ( $multisection ) {
2662 ($extra_sections, $extra_lines) =
2663 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2664 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2666 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2668 push @detail_items, @$extra_lines if $extra_lines;
2670 $self->_items_sections( $late_sections, # this could stand a refactor
2672 $escape_function_nonbsp,
2676 if ($conf->exists('svc_phone_sections')) {
2677 my ($phone_sections, $phone_lines) =
2678 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2679 push @{$late_sections}, @$phone_sections;
2680 push @detail_items, @$phone_lines;
2683 push @sections, { 'description' => '', 'subtotal' => '' };
2686 unless ( $conf->exists('disable_previous_balance')
2687 || $conf->exists('previous_balance-summary_only')
2691 warn "$me adding previous balances\n"
2694 foreach my $line_item ( $self->_items_previous ) {
2697 ext_description => [],
2699 $detail->{'ref'} = $line_item->{'pkgnum'};
2700 $detail->{'quantity'} = 1;
2701 $detail->{'section'} = $previous_section;
2702 $detail->{'description'} = &$escape_function($line_item->{'description'});
2703 if ( exists $line_item->{'ext_description'} ) {
2704 @{$detail->{'ext_description'}} = map {
2705 &$escape_function($_);
2706 } @{$line_item->{'ext_description'}};
2708 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2709 $line_item->{'amount'};
2710 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2712 push @detail_items, $detail;
2713 push @buf, [ $detail->{'description'},
2714 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2720 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2721 push @buf, ['','-----------'];
2722 push @buf, [ 'Total Previous Balance',
2723 $money_char. sprintf("%10.2f", $pr_total) ];
2727 if ( $conf->exists('svc_phone-did-summary') ) {
2728 warn "$me adding DID summary\n"
2731 my ($didsummary,$minutes) = $self->_did_summary;
2732 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2734 { 'description' => $didsummary_desc,
2735 'ext_description' => [ $didsummary, $minutes ],
2740 foreach my $section (@sections, @$late_sections) {
2742 warn "$me adding $section section\n"
2745 # begin some normalization
2746 $section->{'subtotal'} = $section->{'amount'}
2748 && !exists($section->{subtotal})
2749 && exists($section->{amount});
2751 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2752 if ( $invoice_data{finance_section} &&
2753 $section->{'description'} eq $invoice_data{finance_section} );
2755 $section->{'subtotal'} = $other_money_char.
2756 sprintf('%.2f', $section->{'subtotal'})
2759 # continue some normalization
2760 $section->{'amount'} = $section->{'subtotal'}
2764 if ( $section->{'description'} ) {
2765 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2770 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2772 $options{'section'} = $section if $multisection;
2773 $options{'format'} = $format;
2774 $options{'escape_function'} = $escape_function;
2775 $options{'format_function'} = sub { () } unless $unsquelched;
2776 $options{'unsquelched'} = $unsquelched;
2777 $options{'summary_page'} = $summarypage;
2778 $options{'skip_usage'} =
2779 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2780 $options{'multilocation'} = $multilocation;
2781 $options{'multisection'} = $multisection;
2783 foreach my $line_item ( $self->_items_pkg(%options) ) {
2785 ext_description => [],
2787 $detail->{'ref'} = $line_item->{'pkgnum'};
2788 $detail->{'quantity'} = $line_item->{'quantity'};
2789 $detail->{'section'} = $section;
2790 $detail->{'description'} = &$escape_function($line_item->{'description'});
2791 if ( exists $line_item->{'ext_description'} ) {
2792 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2794 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2795 $line_item->{'amount'};
2796 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2797 $line_item->{'unit_amount'};
2798 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2800 push @detail_items, $detail;
2801 push @buf, ( [ $detail->{'description'},
2802 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2804 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2808 if ( $section->{'description'} ) {
2809 push @buf, ( ['','-----------'],
2810 [ $section->{'description'}. ' sub-total',
2811 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2820 $invoice_data{current_less_finance} =
2821 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2823 if ( $multisection && !$conf->exists('disable_previous_balance')
2824 || $conf->exists('previous_balance-summary_only') )
2826 unshift @sections, $previous_section if $pr_total;
2829 warn "$me adding taxes\n"
2832 foreach my $tax ( $self->_items_tax ) {
2834 $taxtotal += $tax->{'amount'};
2836 my $description = &$escape_function( $tax->{'description'} );
2837 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2839 if ( $multisection ) {
2841 my $money = $old_latex ? '' : $money_char;
2842 push @detail_items, {
2843 ext_description => [],
2846 description => $description,
2847 amount => $money. $amount,
2849 section => $tax_section,
2854 push @total_items, {
2855 'total_item' => $description,
2856 'total_amount' => $other_money_char. $amount,
2861 push @buf,[ $description,
2862 $money_char. $amount,
2869 $total->{'total_item'} = 'Sub-total';
2870 $total->{'total_amount'} =
2871 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2873 if ( $multisection ) {
2874 $tax_section->{'subtotal'} = $other_money_char.
2875 sprintf('%.2f', $taxtotal);
2876 $tax_section->{'pretotal'} = 'New charges sub-total '.
2877 $total->{'total_amount'};
2878 push @sections, $tax_section if $taxtotal;
2880 unshift @total_items, $total;
2883 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2885 push @buf,['','-----------'];
2886 push @buf,[( $conf->exists('disable_previous_balance')
2888 : 'Total New Charges'
2890 $money_char. sprintf("%10.2f",$self->charged) ];
2896 $item = $conf->config('previous_balance-exclude_from_total')
2897 || 'Total New Charges'
2898 if $conf->exists('previous_balance-exclude_from_total');
2899 my $amount = $self->charged +
2900 ( $conf->exists('disable_previous_balance') ||
2901 $conf->exists('previous_balance-exclude_from_total')
2905 $total->{'total_item'} = &$embolden_function($item);
2906 $total->{'total_amount'} =
2907 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2908 if ( $multisection ) {
2909 if ( $adjust_section->{'sort_weight'} ) {
2910 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2911 sprintf("%.2f", ($self->billing_balance || 0) );
2913 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2914 sprintf('%.2f', $self->charged );
2917 push @total_items, $total;
2919 push @buf,['','-----------'];
2922 sprintf( '%10.2f', $amount )
2927 unless ( $conf->exists('disable_previous_balance') ) {
2928 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2931 my $credittotal = 0;
2932 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2935 $total->{'total_item'} = &$escape_function($credit->{'description'});
2936 $credittotal += $credit->{'amount'};
2937 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2938 $adjusttotal += $credit->{'amount'};
2939 if ( $multisection ) {
2940 my $money = $old_latex ? '' : $money_char;
2941 push @detail_items, {
2942 ext_description => [],
2945 description => &$escape_function($credit->{'description'}),
2946 amount => $money. $credit->{'amount'},
2948 section => $adjust_section,
2951 push @total_items, $total;
2955 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2958 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2959 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2963 my $paymenttotal = 0;
2964 foreach my $payment ( $self->_items_payments ) {
2966 $total->{'total_item'} = &$escape_function($payment->{'description'});
2967 $paymenttotal += $payment->{'amount'};
2968 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2969 $adjusttotal += $payment->{'amount'};
2970 if ( $multisection ) {
2971 my $money = $old_latex ? '' : $money_char;
2972 push @detail_items, {
2973 ext_description => [],
2976 description => &$escape_function($payment->{'description'}),
2977 amount => $money. $payment->{'amount'},
2979 section => $adjust_section,
2982 push @total_items, $total;
2984 push @buf, [ $payment->{'description'},
2985 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2988 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2990 if ( $multisection ) {
2991 $adjust_section->{'subtotal'} = $other_money_char.
2992 sprintf('%.2f', $adjusttotal);
2993 push @sections, $adjust_section
2994 unless $adjust_section->{sort_weight};
2999 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3000 $total->{'total_amount'} =
3001 &$embolden_function(
3002 $other_money_char. sprintf('%.2f', $summarypage
3004 $self->billing_balance
3005 : $self->owed + $pr_total
3008 if ( $multisection && !$adjust_section->{sort_weight} ) {
3009 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3010 $total->{'total_amount'};
3012 push @total_items, $total;
3014 push @buf,['','-----------'];
3015 push @buf,[$self->balance_due_msg, $money_char.
3016 sprintf("%10.2f", $balance_due ) ];
3020 if ( $multisection ) {
3021 if ($conf->exists('svc_phone_sections')) {
3023 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3024 $total->{'total_amount'} =
3025 &$embolden_function(
3026 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3028 my $last_section = pop @sections;
3029 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3030 $total->{'total_amount'};
3031 push @sections, $last_section;
3033 push @sections, @$late_sections
3037 my @includelist = ();
3038 push @includelist, 'summary' if $summarypage;
3039 foreach my $include ( @includelist ) {
3041 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3044 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3046 @inc_src = $conf->config($inc_file, $agentnum);
3050 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3052 my $convert_map = $convert_maps{$format}{$include};
3054 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3055 s/--\@\]/$delimiters{$format}[1]/g;
3058 &$convert_map( $conf->config($inc_file, $agentnum) );
3062 my $inc_tt = new Text::Template (
3064 SOURCE => [ map "$_\n", @inc_src ],
3065 DELIMITERS => $delimiters{$format},
3066 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3068 unless ( $inc_tt->compile() ) {
3069 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3070 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3074 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3076 $invoice_data{$include} =~ s/\n+$//
3077 if ($format eq 'latex');
3082 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3083 /invoice_lines\((\d*)\)/;
3084 $invoice_lines += $1 || scalar(@buf);
3087 die "no invoice_lines() functions in template?"
3088 if ( $format eq 'template' && !$wasfunc );
3090 if ($format eq 'template') {
3092 if ( $invoice_lines ) {
3093 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3094 $invoice_data{'total_pages'}++
3095 if scalar(@buf) % $invoice_lines;
3098 #setup subroutine for the template
3099 sub FS::cust_bill::_template::invoice_lines {
3100 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3102 scalar(@FS::cust_bill::_template::buf)
3103 ? shift @FS::cust_bill::_template::buf
3112 push @collect, split("\n",
3113 $text_template->fill_in( HASH => \%invoice_data,
3114 PACKAGE => 'FS::cust_bill::_template'
3117 $FS::cust_bill::_template::page++;
3119 map "$_\n", @collect;
3121 warn "filling in template for invoice ". $self->invnum. "\n"
3123 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3126 $text_template->fill_in(HASH => \%invoice_data);
3130 # helper routine for generating date ranges
3131 sub _prior_month30s {
3134 [ 1, 2592000 ], # 0-30 days ago
3135 [ 2592000, 5184000 ], # 30-60 days ago
3136 [ 5184000, 7776000 ], # 60-90 days ago
3137 [ 7776000, 0 ], # 90+ days ago
3140 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3141 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3146 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3148 Returns an postscript invoice, as a scalar.
3150 Options can be passed as a hashref (recommended) or as a list of time, template
3151 and then any key/value pairs for any other options.
3153 I<time> an optional value used to control the printing of overdue messages. The
3154 default is now. It isn't the date of the invoice; that's the `_date' field.
3155 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3156 L<Time::Local> and L<Date::Parse> for conversion functions.
3158 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3165 my ($file, $lfile) = $self->print_latex(@_);
3166 my $ps = generate_ps($file);
3172 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3174 Returns an PDF invoice, as a scalar.
3176 Options can be passed as a hashref (recommended) or as a list of time, template
3177 and then any key/value pairs for any other options.
3179 I<time> an optional value used to control the printing of overdue messages. The
3180 default is now. It isn't the date of the invoice; that's the `_date' field.
3181 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3182 L<Time::Local> and L<Date::Parse> for conversion functions.
3184 I<template>, if specified, is the name of a suffix for alternate invoices.
3186 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3193 my ($file, $lfile) = $self->print_latex(@_);
3194 my $pdf = generate_pdf($file);
3200 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3202 Returns an HTML invoice, as a scalar.
3204 I<time> an optional value used to control the printing of overdue messages. The
3205 default is now. It isn't the date of the invoice; that's the `_date' field.
3206 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3207 L<Time::Local> and L<Date::Parse> for conversion functions.
3209 I<template>, if specified, is the name of a suffix for alternate invoices.
3211 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3213 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3214 when emailing the invoice as part of a multipart/related MIME email.
3222 %params = %{ shift() };
3224 $params{'time'} = shift;
3225 $params{'template'} = shift;
3226 $params{'cid'} = shift;
3229 $params{'format'} = 'html';
3231 $self->print_generic( %params );
3234 # quick subroutine for print_latex
3236 # There are ten characters that LaTeX treats as special characters, which
3237 # means that they do not simply typeset themselves:
3238 # # $ % & ~ _ ^ \ { }
3240 # TeX ignores blanks following an escaped character; if you want a blank (as
3241 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3245 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3246 $value =~ s/([<>])/\$$1\$/g;
3252 encode_entities($value);
3256 sub _html_escape_nbsp {
3257 my $value = _html_escape(shift);
3258 $value =~ s/ +/ /g;
3262 #utility methods for print_*
3264 sub _translate_old_latex_format {
3265 warn "_translate_old_latex_format called\n"
3272 if ( $line =~ /^%%Detail\s*$/ ) {
3274 push @template, q![@--!,
3275 q! foreach my $_tr_line (@detail_items) {!,
3276 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3277 q! $_tr_line->{'description'} .= !,
3278 q! "\\tabularnewline\n~~".!,
3279 q! join( "\\tabularnewline\n~~",!,
3280 q! @{$_tr_line->{'ext_description'}}!,
3284 while ( ( my $line_item_line = shift )
3285 !~ /^%%EndDetail\s*$/ ) {
3286 $line_item_line =~ s/'/\\'/g; # nice LTS
3287 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3288 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3289 push @template, " \$OUT .= '$line_item_line';";
3292 push @template, '}',
3295 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3297 push @template, '[@--',
3298 ' foreach my $_tr_line (@total_items) {';
3300 while ( ( my $total_item_line = shift )
3301 !~ /^%%EndTotalDetails\s*$/ ) {
3302 $total_item_line =~ s/'/\\'/g; # nice LTS
3303 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3304 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3305 push @template, " \$OUT .= '$total_item_line';";
3308 push @template, '}',
3312 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3313 push @template, $line;
3319 warn "$_\n" foreach @template;
3328 #check for an invoice-specific override
3329 return $self->invoice_terms if $self->invoice_terms;
3331 #check for a customer- specific override
3332 my $cust_main = $self->cust_main;
3333 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3335 #use configured default
3336 $conf->config('invoice_default_terms') || '';
3342 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3343 $duedate = $self->_date() + ( $1 * 86400 );
3350 $self->due_date ? time2str(shift, $self->due_date) : '';
3353 sub balance_due_msg {
3355 my $msg = 'Balance Due';
3356 return $msg unless $self->terms;
3357 if ( $self->due_date ) {
3358 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3359 } elsif ( $self->terms ) {
3360 $msg .= ' - '. $self->terms;
3365 sub balance_due_date {
3368 if ( $conf->exists('invoice_default_terms')
3369 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3370 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3375 =item invnum_date_pretty
3377 Returns a string with the invoice number and date, for example:
3378 "Invoice #54 (3/20/2008)"
3382 sub invnum_date_pretty {
3384 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3389 Returns a string with the date, for example: "3/20/2008"
3395 time2str($date_format, $self->_date);
3398 use vars qw(%pkg_category_cache);
3399 sub _items_sections {
3402 my $summarypage = shift;
3404 my $extra_sections = shift;
3408 my %late_subtotal = ();
3411 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3414 my $usage = $cust_bill_pkg->usage;
3416 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3417 next if ( $display->summary && $summarypage );
3419 my $section = $display->section;
3420 my $type = $display->type;
3422 $not_tax{$section} = 1
3423 unless $cust_bill_pkg->pkgnum == 0;
3425 if ( $display->post_total && !$summarypage ) {
3426 if (! $type || $type eq 'S') {
3427 $late_subtotal{$section} += $cust_bill_pkg->setup
3428 if $cust_bill_pkg->setup != 0;
3432 $late_subtotal{$section} += $cust_bill_pkg->recur
3433 if $cust_bill_pkg->recur != 0;
3436 if ($type && $type eq 'R') {
3437 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3438 if $cust_bill_pkg->recur != 0;
3441 if ($type && $type eq 'U') {
3442 $late_subtotal{$section} += $usage
3443 unless scalar(@$extra_sections);
3448 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3450 if (! $type || $type eq 'S') {
3451 $subtotal{$section} += $cust_bill_pkg->setup
3452 if $cust_bill_pkg->setup != 0;
3456 $subtotal{$section} += $cust_bill_pkg->recur
3457 if $cust_bill_pkg->recur != 0;
3460 if ($type && $type eq 'R') {
3461 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3462 if $cust_bill_pkg->recur != 0;
3465 if ($type && $type eq 'U') {
3466 $subtotal{$section} += $usage
3467 unless scalar(@$extra_sections);
3476 %pkg_category_cache = ();
3478 push @$late, map { { 'description' => &{$escape}($_),
3479 'subtotal' => $late_subtotal{$_},
3481 'sort_weight' => ( _pkg_category($_)
3482 ? _pkg_category($_)->weight
3485 ((_pkg_category($_) && _pkg_category($_)->condense)
3486 ? $self->_condense_section($format)
3490 sort _sectionsort keys %late_subtotal;
3493 if ( $summarypage ) {
3494 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3495 map { $_->categoryname } qsearch('pkg_category', {});
3496 push @sections, '' if exists($subtotal{''});
3498 @sections = keys %subtotal;
3501 my @early = map { { 'description' => &{$escape}($_),
3502 'subtotal' => $subtotal{$_},
3503 'summarized' => $not_tax{$_} ? '' : 'Y',
3504 'tax_section' => $not_tax{$_} ? '' : 'Y',
3505 'sort_weight' => ( _pkg_category($_)
3506 ? _pkg_category($_)->weight
3509 ((_pkg_category($_) && _pkg_category($_)->condense)
3510 ? $self->_condense_section($format)
3515 push @early, @$extra_sections if $extra_sections;
3517 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3521 #helper subs for above
3524 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3528 my $categoryname = shift;
3529 $pkg_category_cache{$categoryname} ||=
3530 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3533 my %condensed_format = (
3534 'label' => [ qw( Description Qty Amount ) ],
3536 sub { shift->{description} },
3537 sub { shift->{quantity} },
3538 sub { my($href, %opt) = @_;
3539 ($opt{dollar} || ''). $href->{amount};
3542 'align' => [ qw( l r r ) ],
3543 'span' => [ qw( 5 1 1 ) ], # unitprices?
3544 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3547 sub _condense_section {
3548 my ( $self, $format ) = ( shift, shift );
3550 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3551 qw( description_generator
3554 total_line_generator
3559 sub _condensed_generator_defaults {
3560 my ( $self, $format ) = ( shift, shift );
3561 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3570 sub _condensed_header_generator {
3571 my ( $self, $format ) = ( shift, shift );
3573 my ( $f, $prefix, $suffix, $separator, $column ) =
3574 _condensed_generator_defaults($format);
3576 if ($format eq 'latex') {
3577 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3578 $suffix = "\\\\\n\\hline";
3581 sub { my ($d,$a,$s,$w) = @_;
3582 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3584 } elsif ( $format eq 'html' ) {
3585 $prefix = '<th></th>';
3589 sub { my ($d,$a,$s,$w) = @_;
3590 return qq!<th align="$html_align{$a}">$d</th>!;
3598 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3600 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3603 $prefix. join($separator, @result). $suffix;
3608 sub _condensed_description_generator {
3609 my ( $self, $format ) = ( shift, shift );
3611 my ( $f, $prefix, $suffix, $separator, $column ) =
3612 _condensed_generator_defaults($format);
3614 my $money_char = '$';
3615 if ($format eq 'latex') {
3616 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3618 $separator = " & \n";
3620 sub { my ($d,$a,$s,$w) = @_;
3621 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3623 $money_char = '\\dollar';
3624 }elsif ( $format eq 'html' ) {
3625 $prefix = '"><td align="center"></td>';
3629 sub { my ($d,$a,$s,$w) = @_;
3630 return qq!<td align="$html_align{$a}">$d</td>!;
3632 #$money_char = $conf->config('money_char') || '$';
3633 $money_char = ''; # this is madness
3641 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3643 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3645 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3646 map { $f->{$_}->[$i] } qw(align span width)
3650 $prefix. join( $separator, @result ). $suffix;
3655 sub _condensed_total_generator {
3656 my ( $self, $format ) = ( shift, shift );
3658 my ( $f, $prefix, $suffix, $separator, $column ) =
3659 _condensed_generator_defaults($format);
3662 if ($format eq 'latex') {
3665 $separator = " & \n";
3667 sub { my ($d,$a,$s,$w) = @_;
3668 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3670 }elsif ( $format eq 'html' ) {
3674 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3676 sub { my ($d,$a,$s,$w) = @_;
3677 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3686 # my $r = &{$f->{fields}->[$i]}(@args);
3687 # $r .= ' Total' unless $i;
3689 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3691 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3692 map { $f->{$_}->[$i] } qw(align span width)
3696 $prefix. join( $separator, @result ). $suffix;
3701 =item total_line_generator FORMAT
3703 Returns a coderef used for generation of invoice total line items for this
3704 usage_class. FORMAT is either html or latex
3708 # should not be used: will have issues with hash element names (description vs
3709 # total_item and amount vs total_amount -- another array of functions?
3711 sub _condensed_total_line_generator {
3712 my ( $self, $format ) = ( shift, shift );
3714 my ( $f, $prefix, $suffix, $separator, $column ) =
3715 _condensed_generator_defaults($format);
3718 if ($format eq 'latex') {
3721 $separator = " & \n";
3723 sub { my ($d,$a,$s,$w) = @_;
3724 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3726 }elsif ( $format eq 'html' ) {
3730 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3732 sub { my ($d,$a,$s,$w) = @_;
3733 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3742 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3744 &{$column}( &{$f->{fields}->[$i]}(@args),
3745 map { $f->{$_}->[$i] } qw(align span width)
3749 $prefix. join( $separator, @result ). $suffix;
3754 #sub _items_extra_usage_sections {
3756 # my $escape = shift;
3758 # my %sections = ();
3760 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3761 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3763 # next unless $cust_bill_pkg->pkgnum > 0;
3765 # foreach my $section ( keys %usage_class ) {
3767 # my $usage = $cust_bill_pkg->usage($section);
3769 # next unless $usage && $usage > 0;
3771 # $sections{$section} ||= 0;
3772 # $sections{$section} += $usage;
3778 # map { { 'description' => &{$escape}($_),
3779 # 'subtotal' => $sections{$_},
3780 # 'summarized' => '',
3781 # 'tax_section' => '',
3784 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3788 sub _items_extra_usage_sections {
3797 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3798 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3799 next unless $cust_bill_pkg->pkgnum > 0;
3801 foreach my $classnum ( keys %usage_class ) {
3802 my $section = $usage_class{$classnum}->classname;
3803 $classnums{$section} = $classnum;
3805 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3806 my $amount = $detail->amount;
3807 next unless $amount && $amount > 0;
3809 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3810 $sections{$section}{amount} += $amount; #subtotal
3811 $sections{$section}{calls}++;
3812 $sections{$section}{duration} += $detail->duration;
3814 my $desc = $detail->regionname;
3815 my $description = $desc;
3816 $description = substr($desc, 0, 50). '...'
3817 if $format eq 'latex' && length($desc) > 50;
3819 $lines{$section}{$desc} ||= {
3820 description => &{$escape}($description),
3821 #pkgpart => $part_pkg->pkgpart,
3822 pkgnum => $cust_bill_pkg->pkgnum,
3827 #unit_amount => $cust_bill_pkg->unitrecur,
3828 quantity => $cust_bill_pkg->quantity,
3829 product_code => 'N/A',
3830 ext_description => [],
3833 $lines{$section}{$desc}{amount} += $amount;
3834 $lines{$section}{$desc}{calls}++;
3835 $lines{$section}{$desc}{duration} += $detail->duration;
3841 my %sectionmap = ();
3842 foreach (keys %sections) {
3843 my $usage_class = $usage_class{$classnums{$_}};
3844 $sectionmap{$_} = { 'description' => &{$escape}($_),
3845 'amount' => $sections{$_}{amount}, #subtotal
3846 'calls' => $sections{$_}{calls},
3847 'duration' => $sections{$_}{duration},
3849 'tax_section' => '',
3850 'sort_weight' => $usage_class->weight,
3851 ( $usage_class->format
3852 ? ( map { $_ => $usage_class->$_($format) }
3853 qw( description_generator header_generator total_generator total_line_generator )
3860 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3864 foreach my $section ( keys %lines ) {
3865 foreach my $line ( keys %{$lines{$section}} ) {
3866 my $l = $lines{$section}{$line};
3867 $l->{section} = $sectionmap{$section};
3868 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3869 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3874 return(\@sections, \@lines);
3880 my $end = $self->_date;
3881 my $start = $end - 2592000; # 30 days
3882 my $cust_main = $self->cust_main;
3883 my @pkgs = $cust_main->all_pkgs;
3884 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3887 foreach my $pkg ( @pkgs ) {
3888 my @h_cust_svc = $pkg->h_cust_svc($end);
3889 foreach my $h_cust_svc ( @h_cust_svc ) {
3890 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3891 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3893 my $inserted = $h_cust_svc->date_inserted;
3894 my $deleted = $h_cust_svc->date_deleted;
3895 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3897 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3899 # DID either activated or ported in; cannot be both for same DID simultaneously
3900 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3901 && (!$phone_inserted->lnp_status
3902 || $phone_inserted->lnp_status eq ''
3903 || $phone_inserted->lnp_status eq 'native')) {
3906 else { # this one not so clean, should probably move to (h_)svc_phone
3907 my $phone_portedin = qsearchs( 'h_svc_phone',
3908 { 'svcnum' => $h_cust_svc->svcnum,
3909 'lnp_status' => 'portedin' },
3910 FS::h_svc_phone->sql_h_searchs($end),
3912 $num_portedin++ if $phone_portedin;
3915 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3916 if($deleted >= $start && $deleted <= $end && $phone_deleted
3917 && (!$phone_deleted->lnp_status
3918 || $phone_deleted->lnp_status ne 'portingout')) {
3921 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3922 && $phone_deleted->lnp_status
3923 && $phone_deleted->lnp_status eq 'portingout') {
3927 # increment usage minutes
3928 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3929 foreach my $cdr ( @cdrs ) {
3930 $minutes += $cdr->billsec/60;
3933 # don't look at this service again
3934 push @seen, $h_cust_svc->svcnum;
3938 $minutes = sprintf("%d", $minutes);
3939 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3940 . "$num_deactivated Ported-Out: $num_portedout ",
3941 "Total Minutes: $minutes");
3944 sub _items_svc_phone_sections {
3953 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3954 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3956 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3957 next unless $cust_bill_pkg->pkgnum > 0;
3959 my @header = $cust_bill_pkg->details_header;
3960 next unless scalar(@header);
3962 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3964 my $phonenum = $detail->phonenum;
3965 next unless $phonenum;
3967 my $amount = $detail->amount;
3968 next unless $amount && $amount > 0;
3970 $sections{$phonenum} ||= { 'amount' => 0,
3973 'sort_weight' => -1,
3974 'phonenum' => $phonenum,
3976 $sections{$phonenum}{amount} += $amount; #subtotal
3977 $sections{$phonenum}{calls}++;
3978 $sections{$phonenum}{duration} += $detail->duration;
3980 my $desc = $detail->regionname;
3981 my $description = $desc;
3982 $description = substr($desc, 0, 50). '...'
3983 if $format eq 'latex' && length($desc) > 50;
3985 $lines{$phonenum}{$desc} ||= {
3986 description => &{$escape}($description),
3987 #pkgpart => $part_pkg->pkgpart,
3995 product_code => 'N/A',
3996 ext_description => [],
3999 $lines{$phonenum}{$desc}{amount} += $amount;
4000 $lines{$phonenum}{$desc}{calls}++;
4001 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4003 my $line = $usage_class{$detail->classnum}->classname;
4004 $sections{"$phonenum $line"} ||=
4008 'sort_weight' => $usage_class{$detail->classnum}->weight,
4009 'phonenum' => $phonenum,
4010 'header' => [ @header ],
4012 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4013 $sections{"$phonenum $line"}{calls}++;
4014 $sections{"$phonenum $line"}{duration} += $detail->duration;
4016 $lines{"$phonenum $line"}{$desc} ||= {
4017 description => &{$escape}($description),
4018 #pkgpart => $part_pkg->pkgpart,
4026 product_code => 'N/A',
4027 ext_description => [],
4030 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4031 $lines{"$phonenum $line"}{$desc}{calls}++;
4032 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4033 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4034 $detail->formatted('format' => $format);
4039 my %sectionmap = ();
4040 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4041 foreach ( keys %sections ) {
4042 my @header = @{ $sections{$_}{header} || [] };
4044 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4045 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4046 my $usage_class = $summary ? $simple : $usage_simple;
4047 my $ending = $summary ? ' usage charges' : '';
4050 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4052 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4053 'amount' => $sections{$_}{amount}, #subtotal
4054 'calls' => $sections{$_}{calls},
4055 'duration' => $sections{$_}{duration},
4057 'tax_section' => '',
4058 'phonenum' => $sections{$_}{phonenum},
4059 'sort_weight' => $sections{$_}{sort_weight},
4060 'post_total' => $summary, #inspire pagebreak
4062 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4063 qw( description_generator
4066 total_line_generator
4073 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4074 $a->{sort_weight} <=> $b->{sort_weight}
4079 foreach my $section ( keys %lines ) {
4080 foreach my $line ( keys %{$lines{$section}} ) {
4081 my $l = $lines{$section}{$line};
4082 $l->{section} = $sectionmap{$section};
4083 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4084 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4089 return(\@sections, \@lines);
4096 #my @display = scalar(@_)
4098 # : qw( _items_previous _items_pkg );
4099 # #: qw( _items_pkg );
4100 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4101 my @display = qw( _items_previous _items_pkg );
4104 foreach my $display ( @display ) {
4105 push @b, $self->$display(@_);
4110 sub _items_previous {
4112 my $cust_main = $self->cust_main;
4113 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4115 foreach ( @pr_cust_bill ) {
4116 my $date = $conf->exists('invoice_show_prior_due_date')
4117 ? 'due '. $_->due_date2str($date_format)
4118 : time2str($date_format, $_->_date);
4120 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4121 #'pkgpart' => 'N/A',
4123 'amount' => sprintf("%.2f", $_->owed),
4129 # 'description' => 'Previous Balance',
4130 # #'pkgpart' => 'N/A',
4131 # 'pkgnum' => 'N/A',
4132 # 'amount' => sprintf("%10.2f", $pr_total ),
4133 # 'ext_description' => [ map {
4134 # "Invoice ". $_->invnum.
4135 # " (". time2str("%x",$_->_date). ") ".
4136 # sprintf("%10.2f", $_->owed)
4137 # } @pr_cust_bill ],
4145 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4146 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4147 if ($options{section} && $options{section}->{condensed}) {
4149 local $Storable::canonical = 1;
4150 foreach ( @items ) {
4152 delete $item->{ref};
4153 delete $item->{ext_description};
4154 my $key = freeze($item);
4155 $itemshash{$key} ||= 0;
4156 $itemshash{$key} ++; # += $item->{quantity};
4158 @items = sort { $a->{description} cmp $b->{description} }
4159 map { my $i = thaw($_);
4160 $i->{quantity} = $itemshash{$_};
4162 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4171 return 0 unless $a->itemdesc cmp $b->itemdesc;
4172 return -1 if $b->itemdesc eq 'Tax';
4173 return 1 if $a->itemdesc eq 'Tax';
4174 return -1 if $b->itemdesc eq 'Other surcharges';
4175 return 1 if $a->itemdesc eq 'Other surcharges';
4176 $a->itemdesc cmp $b->itemdesc;
4181 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4182 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4185 sub _items_cust_bill_pkg {
4187 my $cust_bill_pkg = shift;
4190 my $format = $opt{format} || '';
4191 my $escape_function = $opt{escape_function} || sub { shift };
4192 my $format_function = $opt{format_function} || '';
4193 my $unsquelched = $opt{unsquelched} || '';
4194 my $section = $opt{section}->{description} if $opt{section};
4195 my $summary_page = $opt{summary_page} || '';
4196 my $multilocation = $opt{multilocation} || '';
4197 my $multisection = $opt{multisection} || '';
4198 my $discount_show_always = 0;
4201 my ($s, $r, $u) = ( undef, undef, undef );
4202 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4205 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4206 && $conf->exists('discount-show-always'));
4208 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4209 if ( $_ && !$cust_bill_pkg->hidden ) {
4210 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4211 $_->{amount} =~ s/^\-0\.00$/0.00/;
4212 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4214 unless ( $_->{amount} == 0 && !$discount_show_always );
4219 foreach my $display ( grep { defined($section)
4220 ? $_->section eq $section
4223 #grep { !$_->summary || !$summary_page } # bunk!
4224 grep { !$_->summary || $multisection }
4225 $cust_bill_pkg->cust_bill_pkg_display
4229 my $type = $display->type;
4231 my $desc = $cust_bill_pkg->desc;
4232 $desc = substr($desc, 0, 50). '...'
4233 if $format eq 'latex' && length($desc) > 50;
4235 my %details_opt = ( 'format' => $format,
4236 'escape_function' => $escape_function,
4237 'format_function' => $format_function,
4240 if ( $cust_bill_pkg->pkgnum > 0 ) {
4242 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4244 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4246 my $description = $desc;
4247 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4250 unless ( $cust_pkg->part_pkg->hide_svc_detail
4251 || $cust_bill_pkg->hidden )
4254 push @d, map &{$escape_function}($_),
4255 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4256 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4258 if ( $multilocation ) {
4259 my $loc = $cust_pkg->location_label;
4260 $loc = substr($loc, 0, 50). '...'
4261 if $format eq 'latex' && length($loc) > 50;
4262 push @d, &{$escape_function}($loc);
4267 push @d, $cust_bill_pkg->details(%details_opt)
4268 if $cust_bill_pkg->recur == 0;
4270 if ( $cust_bill_pkg->hidden ) {
4271 $s->{amount} += $cust_bill_pkg->setup;
4272 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4273 push @{ $s->{ext_description} }, @d;
4276 description => $description,
4277 #pkgpart => $part_pkg->pkgpart,
4278 pkgnum => $cust_bill_pkg->pkgnum,
4279 amount => $cust_bill_pkg->setup,
4280 unit_amount => $cust_bill_pkg->unitsetup,
4281 quantity => $cust_bill_pkg->quantity,
4282 ext_description => \@d,
4288 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4289 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4290 ( !$type || $type eq 'R' || $type eq 'U' )
4294 my $is_summary = $display->summary;
4295 my $description = ($is_summary && $type && $type eq 'U')
4296 ? "Usage charges" : $desc;
4298 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4299 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4300 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4305 #at least until cust_bill_pkg has "past" ranges in addition to
4306 #the "future" sdate/edate ones... see #3032
4307 my @dates = ( $self->_date );
4308 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4309 push @dates, $prev->sdate if $prev;
4310 push @dates, undef if !$prev;
4312 unless ( $cust_pkg->part_pkg->hide_svc_detail
4313 || $cust_bill_pkg->itemdesc
4314 || $cust_bill_pkg->hidden
4315 || $is_summary && $type && $type eq 'U' )
4318 push @d, map &{$escape_function}($_),
4319 $cust_pkg->h_labels_short(@dates, 'I')
4320 #$cust_bill_pkg->edate,
4321 #$cust_bill_pkg->sdate)
4322 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4324 if ( $multilocation ) {
4325 my $loc = $cust_pkg->location_label;
4326 $loc = substr($loc, 0, 50). '...'
4327 if $format eq 'latex' && length($loc) > 50;
4328 push @d, &{$escape_function}($loc);
4333 push @d, $cust_bill_pkg->details(%details_opt)
4334 unless ($is_summary || $type && $type eq 'R');
4338 $amount = $cust_bill_pkg->recur;
4339 }elsif($type eq 'R') {
4340 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4341 }elsif($type eq 'U') {
4342 $amount = $cust_bill_pkg->usage;
4345 if ( !$type || $type eq 'R' ) {
4347 if ( $cust_bill_pkg->hidden ) {
4348 $r->{amount} += $amount;
4349 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4350 push @{ $r->{ext_description} }, @d;
4353 description => $description,
4354 #pkgpart => $part_pkg->pkgpart,
4355 pkgnum => $cust_bill_pkg->pkgnum,
4357 unit_amount => $cust_bill_pkg->unitrecur,
4358 quantity => $cust_bill_pkg->quantity,
4359 ext_description => \@d,
4363 } else { # $type eq 'U'
4365 if ( $cust_bill_pkg->hidden ) {
4366 $u->{amount} += $amount;
4367 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4368 push @{ $u->{ext_description} }, @d;
4371 description => $description,
4372 #pkgpart => $part_pkg->pkgpart,
4373 pkgnum => $cust_bill_pkg->pkgnum,
4375 unit_amount => $cust_bill_pkg->unitrecur,
4376 quantity => $cust_bill_pkg->quantity,
4377 ext_description => \@d,
4383 } # recurring or usage with recurring charge
4385 } else { #pkgnum tax or one-shot line item (??)
4387 if ( $cust_bill_pkg->setup != 0 ) {
4389 'description' => $desc,
4390 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4393 if ( $cust_bill_pkg->recur != 0 ) {
4395 'description' => "$desc (".
4396 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4397 time2str($date_format, $cust_bill_pkg->edate). ')',
4398 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4408 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4410 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4411 $_->{amount} =~ s/^\-0\.00$/0.00/;
4412 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4414 unless ( $_->{amount} == 0 && !$discount_show_always );
4422 sub _items_credits {
4423 my( $self, %opt ) = @_;
4424 my $trim_len = $opt{'trim_len'} || 60;
4428 foreach ( $self->cust_credited ) {
4430 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4432 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4433 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4434 $reason = " ($reason) " if $reason;
4437 #'description' => 'Credit ref\#'. $_->crednum.
4438 # " (". time2str("%x",$_->cust_credit->_date) .")".
4440 'description' => 'Credit applied '.
4441 time2str($date_format,$_->cust_credit->_date). $reason,
4442 'amount' => sprintf("%.2f",$_->amount),
4450 sub _items_payments {
4454 #get & print payments
4455 foreach ( $self->cust_bill_pay ) {
4457 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4460 'description' => "Payment received ".
4461 time2str($date_format,$_->cust_pay->_date ),
4462 'amount' => sprintf("%.2f", $_->amount )
4470 =item call_details [ OPTION => VALUE ... ]
4472 Returns an array of CSV strings representing the call details for this invoice
4473 The only option available is the boolean prepend_billed_number
4478 my ($self, %opt) = @_;
4480 my $format_function = sub { shift };
4482 if ($opt{prepend_billed_number}) {
4483 $format_function = sub {
4487 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4492 my @details = map { $_->details( 'format_function' => $format_function,
4493 'escape_function' => sub{ return() },
4497 $self->cust_bill_pkg;
4498 my $header = $details[0];
4499 ( $header, grep { $_ ne $header } @details );
4509 =item process_reprint
4513 sub process_reprint {
4514 process_re_X('print', @_);
4517 =item process_reemail
4521 sub process_reemail {
4522 process_re_X('email', @_);
4530 process_re_X('fax', @_);
4538 process_re_X('ftp', @_);
4545 sub process_respool {
4546 process_re_X('spool', @_);
4549 use Storable qw(thaw);
4553 my( $method, $job ) = ( shift, shift );
4554 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4556 my $param = thaw(decode_base64(shift));
4557 warn Dumper($param) if $DEBUG;
4568 my($method, $job, %param ) = @_;
4570 warn "re_X $method for job $job with param:\n".
4571 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4574 #some false laziness w/search/cust_bill.html
4576 my $orderby = 'ORDER BY cust_bill._date';
4578 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4580 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4582 my @cust_bill = qsearch( {
4583 #'select' => "cust_bill.*",
4584 'table' => 'cust_bill',
4585 'addl_from' => $addl_from,
4587 'extra_sql' => $extra_sql,
4588 'order_by' => $orderby,
4592 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4594 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4597 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4598 foreach my $cust_bill ( @cust_bill ) {
4599 $cust_bill->$method();
4601 if ( $job ) { #progressbar foo
4603 if ( time - $min_sec > $last ) {
4604 my $error = $job->update_statustext(
4605 int( 100 * $num / scalar(@cust_bill) )
4607 die $error if $error;
4618 =head1 CLASS METHODS
4624 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4629 my ($class, $start, $end) = @_;
4631 $class->paid_sql($start, $end). ' - '.
4632 $class->credited_sql($start, $end);
4637 Returns an SQL fragment to retreive the net amount (charged minus credited).
4642 my ($class, $start, $end) = @_;
4643 'charged - '. $class->credited_sql($start, $end);
4648 Returns an SQL fragment to retreive the amount paid against this invoice.
4653 my ($class, $start, $end) = @_;
4654 $start &&= "AND cust_bill_pay._date <= $start";
4655 $end &&= "AND cust_bill_pay._date > $end";
4656 $start = '' unless defined($start);
4657 $end = '' unless defined($end);
4658 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4659 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4664 Returns an SQL fragment to retreive the amount credited against this invoice.
4669 my ($class, $start, $end) = @_;
4670 $start &&= "AND cust_credit_bill._date <= $start";
4671 $end &&= "AND cust_credit_bill._date > $end";
4672 $start = '' unless defined($start);
4673 $end = '' unless defined($end);
4674 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4675 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4680 Returns an SQL fragment to retrieve the due date of an invoice.
4681 Currently only supported on PostgreSQL.
4689 cust_bill.invoice_terms,
4690 cust_main.invoice_terms,
4691 \''.($conf->config('invoice_default_terms') || '').'\'
4692 ), E\'Net (\\\\d+)\'
4694 ) * 86400 + cust_bill._date'
4697 =item search_sql_where HASHREF
4699 Class method which returns an SQL WHERE fragment to search for parameters
4700 specified in HASHREF. Valid parameters are
4706 List reference of start date, end date, as UNIX timestamps.
4716 List reference of charged limits (exclusive).
4720 List reference of charged limits (exclusive).
4724 flag, return open invoices only
4728 flag, return net invoices only
4732 =item newest_percust
4736 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4740 sub search_sql_where {
4741 my($class, $param) = @_;
4743 warn "$me search_sql_where called with params: \n".
4744 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4750 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4751 push @search, "cust_main.agentnum = $1";
4755 if ( $param->{_date} ) {
4756 my($beginning, $ending) = @{$param->{_date}};
4758 push @search, "cust_bill._date >= $beginning",
4759 "cust_bill._date < $ending";
4763 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4764 push @search, "cust_bill.invnum >= $1";
4766 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4767 push @search, "cust_bill.invnum <= $1";
4771 if ( $param->{charged} ) {
4772 my @charged = ref($param->{charged})
4773 ? @{ $param->{charged} }
4774 : ($param->{charged});
4776 push @search, map { s/^charged/cust_bill.charged/; $_; }
4780 my $owed_sql = FS::cust_bill->owed_sql;
4783 if ( $param->{owed} ) {
4784 my @owed = ref($param->{owed})
4785 ? @{ $param->{owed} }
4787 push @search, map { s/^owed/$owed_sql/; $_; }
4792 push @search, "0 != $owed_sql"
4793 if $param->{'open'};
4794 push @search, '0 != '. FS::cust_bill->net_sql
4798 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4799 if $param->{'days'};
4802 if ( $param->{'newest_percust'} ) {
4804 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4805 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4807 my @newest_where = map { my $x = $_;
4808 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4811 grep ! /^cust_main./, @search;
4812 my $newest_where = scalar(@newest_where)
4813 ? ' AND '. join(' AND ', @newest_where)
4817 push @search, "cust_bill._date = (
4818 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4819 WHERE newest_cust_bill.custnum = cust_bill.custnum
4825 #agent virtualization
4826 my $curuser = $FS::CurrentUser::CurrentUser;
4827 if ( $curuser->username eq 'fs_queue'
4828 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4830 my $newuser = qsearchs('access_user', {
4831 'username' => $username,
4835 $curuser = $newuser;
4837 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4840 push @search, $curuser->agentnums_sql;
4842 join(' AND ', @search );
4854 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4855 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base